-
Hahn Axel (hahn) authoredHahn Axel (hahn) authored
project.class.php 102.96 KiB
<?php
define("OPTION_DEFAULT", -999);
define("OPTION_NONE", -1);
require_once __DIR__ . '/../inc_functions.php';
require_once 'base.class.php';
require_once 'messenger.class.php';
// plugins
// require_once 'plugins.class.php';
require_once 'build_base.class.php';
require_once 'rollout_base.class.php';
require_once 'htmlguielements.class.php';
/* ######################################################################
IML DEPLOYMENT
class project for all actions for single project
* actions
* rendering html
---------------------------------------------------------------------
2013-11-08 Axel <axel.hahn@iml.unibe.ch>
(...)
2024-08-28 Axel php8 only; added variable types; short array syntax
2024-09-20 Axel build(): check if checkout of sources failed
###################################################################### */
/**
* class for single project
*/
// class project {
class project extends base
{
// ----------------------------------------------------------------------
// CONFIG
// ----------------------------------------------------------------------
/**
* configuration ($aConfig in the config file)
* @var array
*/
protected array $_aConfig = [];
/**
* configuration of the project (= $aProjects[ID] in the config file)
* @var array
*/
protected array $_aPrjConfig = [];
/**
* version infos of all phases
* @var array
*/
protected array $_aData = [];
/**
* existing versions in the archive dir
* @var array
*/
protected array $_aVersions = [];
/**
* output file to fetch processing content with ajax request
* @var string
*/
protected string $_sProcessTempOut = '';
/**
* places of version infos in each deployment phase
* @var array
*/
protected array $_aPlaces = [
"onhold" => "Queue",
"ready2install" => "Puppet",
"deployed" => "Installiert",
];
/**
* collector for returncodes of multiple exec calls
* @var int
*/
protected int $_iRcAll = 0;
/**
* reference to html renderer class to draw output items
* @var object
*/
protected object $_oHtml;
/**
* object to access a version control, .e. git
* @var object
*/
protected object $_oVcs;
/**
* object for rollout
* @var object
*/
public object $oRolloutPlugin;
/**
* Name of the current branch
* @var string
*/
protected string $_sBranchname = '';
/**
* messenger object to send messages
* @var object
*/
protected object $oMessenger;
/**
* collected errors
* @var array
*/
protected array $_errors = [];
// ----------------------------------------------------------------------
// constructor
// ----------------------------------------------------------------------
/**
* constructor
* @param string $sId id of the project
*/
public function __construct(string $sId = '')
{
$this->oUser = new user();
$this->_oHtml = new htmlguielements();
$this->_readConfig();
if ($sId) {
$this->setProjectById($sId);
}
}
// ----------------------------------------------------------------------
// private functions
// ----------------------------------------------------------------------
/**
* add a log messsage
* @global object $oLog
* @param string $sMessage messeage text
* @param string $sLevel warnlevel of the given message
* @return bool
*/
protected function log(string $sMessage, string $sLevel = "info")
{
global $oCLog;
return $oCLog->add(basename(__FILE__) . " class " . __CLASS__ . " - " . $sMessage, $sLevel);
}
/**
* send info messages to project specific targets (Slack, E-Mail)
* @param string $sMessage
* @return boolean
*/
protected function _sendMessage($sMessage)
{
$aConfig = [];
if (isset($this->_aPrjConfig['messenger']['slack'])) {
$sSlack = $this->_aPrjConfig['messenger']['slack'];
$aConfig['slack'] = ['incomingurl' => $sSlack];
foreach (['user', 'icon'] as $sKey) {
if (isset($this->_aConfig['messenger']['slack']['presets'][$sSlack][$sKey])) {
$aConfig['slack'][$sKey] = $this->_aConfig['messenger']['slack']['presets'][$sSlack][$sKey];
}
}
}
if (isset($this->_aConfig['messenger']['email'])) {
$aConfig['email'] = $this->_aConfig['messenger']['email'];
$aConfig['email']['to'] = $this->_aPrjConfig['messenger']['email'];
}
if (!count($aConfig)) {
return false;
}
// init on first usage
if (!isset($this->oMessenger) || !$this->oMessenger) {
$this->oMessenger = new messenger($aConfig);
}
// add some metadata to the message body
$sText = $this->getLabel() . ': ' . html_entity_decode($sMessage) . "\n"
. t('page-login-username') . ": " . $this->oUser->getUsername() . "\n";
if (isset($_SERVER) && is_array($_SERVER)) {
if (isset($_SERVER['HTTP_HOST'])) {
$sText .= t('project-home') . ': ' . $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . '/deployment/' . $this->getId() . "\n";
}
/*
if(array_key_exists('HTTP_ORIGIN', $_SERVER)){
$sText.= t('project-home').": <".$_SERVER['HTTP_ORIGIN'].'/deployment/'.$this->getId()."/>\n";
}
*/
}
$this->oMessenger->sendMessage($sText);
return true;
}
/**
* read default config file
* @return boolean
*/
protected function _readConfig(): bool
{
global $aConfig;
$this->_aConfig = $aConfig;
return true;
}
/**
* validate config data
* @return boolean
*/
protected function _verifyConfig(): bool
{
if (!is_array($this->_aPrjConfig) || !count($this->_aPrjConfig)) {
// die(t("class-project-error-no-config"));
throw new Exception(t("class-project-error-no-config"));
}
if (!isset($this->_aConfig["packageDir"])) {
die(t("class-project-error-no-packagedir"));
}
if (!$this->_aConfig["packageDir"]) {
die(t("class-project-error-packagedir-empty"));
}
if (!file_exists($this->_aConfig["packageDir"])) {
die(sprintf(t("class-project-error-packagedir-does-not-exist"), $this->_aConfig['packageDir']));
}
if (!isset($this->_aConfig["archiveDir"])) {
die(t("class-project-error-no-archivedir"));
}
if (!$this->_aConfig["archiveDir"]) {
die(t("class-project-error-archivedir-empty"));
}
if (!file_exists($this->_aConfig["archiveDir"])) {
die(sprintf(t("class-project-error-packagedir-does-not-exist"), $this->_aConfig['archiveDir']));
}
foreach (["fileprefix", "build", "phases"] as $sKey) {
if (!isset($this->_aPrjConfig[$sKey])) {
die(sprintf(t("class-project-error-missing-prjkey"), $sKey, print_r($this->_aPrjConfig, true)));
}
}
// TODO: verify ausbauen
/*
if (!$this->_aConfig["dataDir"]) {
die(t("class-project-error-datadir-empty"));
}
if (!file_exists($this->_aConfig["dataDir"])) {
die(sprintf(t("class-project-error-data-does-not-exist"), $this->_aConfig['dataDir']));
}
foreach (["database", "projects", "sshkeys"] as $sKey) {
$sTestDir=$this->_aConfig["dataDir"]."/$sKey";
if (!file_exists($sTestDir)) {
mkdir($sTestDir);
// die(sprintf(t("class-project-error-missing-prjkey"), $sKey, print_r($this->_aPrjConfig, true)));
}
}
*/
return true;
}
/**
* execute a commandline; returns a string of output of timestamp, command, output and returncode
* @param string $sCommand command to execute
* @param bool $bFlush flush content of output buffer
* @return string
*/
protected function _execAndSend(string $sCommand, bool $bFlush = false): string
{
$this->log(__FUNCTION__ . " start");
$sReturn = '';
$bUseHtml = $_SERVER ? true : false;
if ($bFlush) {
ob_implicit_flush(true);
}
// ob_end_flush();
// stderr ausgeben
$sCommand .= ' 2>&1';
$sReturn .= "[" . date("H:i:s d.m.Y") . "] ";
$sReturn .= $bUseHtml ? "<strong>$sCommand</strong>" : "$sCommand";
$sReturn .= $bUseHtml ? "<br>" : "\n";
$sOutput = false;
$this->log(__FUNCTION__ . " start $sCommand");
exec($sCommand, $aOutput, $iRc);
$this->log(__FUNCTION__ . " ended command $sCommand");
$sReturn .= (count($aOutput)) ? htmlentities(implode("\n", $aOutput)) . "\n" : "";
/*
$descriptorspec = [
0 => ["pipe", "r"], // stdin is a pipe that the child will read from
1 => ["pipe", "w"], // stdout is a pipe that the child will write to
2 => ["pipe", "w"] // stderr is a pipe that the child will write to
];
if ($bFlush) {
flush();
}
$process = proc_open($sCommand, $descriptorspec, $pipes, realpath('./'), []);
$sErrors = false;
if (is_resource($process)) {
while ($s = fgets($pipes[1])) {
$sReturn.=$s;
if ($bFlush) {
flush();
}
}
while ($s = fgets($pipes[2])) {
$sErrors.=$s;
if ($bFlush) {
flush();
}
}
}
if ($sErrors) {
$sReturn.="STDERR:\n" . $sErrors;
}
$oStatus = proc_get_status($process);
$iRc = $oStatus['exitcode'];
*/
if ($iRc != 0) {
$this->_logaction("command failed: $sCommand - rc=" . $iRc, __FUNCTION__, "error");
}
$this->_iRcAll += $iRc;
$sReturn .= "[" . date("H:i:s d.m.Y") . "] " . t("exitcode") . " " . $iRc;
if ($bUseHtml) {
if ($iRc == 0) {
$sReturn = '<pre class="cli">' . $sReturn;
} else {
$sReturn = '<pre class="cli error">' . $sReturn;
}
$sReturn .= '</pre>';
}
if ($bFlush) {
flush();
}
return $sReturn;
}
/**
* add an action log message for the current project
* @param string $sMessage message text
* @param string $sAction project action i.e. build, deploy, ...
* @param string $sLoglevel loglevel; default: info
* @return void
*/
protected function _logaction(string $sMessage, string $sAction = "", string $sLoglevel = "info"): void
{
require_once("actionlog.class.php");
$oLog = new Actionlog($this->_aConfig["id"]);
$oLog->add($sMessage, (__CLASS__ . "->" . $sAction), $sLoglevel);
}
// ----------------------------------------------------------------------
// GETTER
// ----------------------------------------------------------------------
/**
* Get filename of fonfigfile for this project
* @param string $sId project id
* @return string
*/
protected function _getConfigFile(string $sId): string
{
if (!$sId) {
die(t("class-project-error-_getConfigFile-requires-id"));
}
return $this->_aConfig["dataDir"] . '/projects/' . $sId . ".json";
}
/**
* Get a full ath for temp directory (for a build)
* @return string
*/
protected function _getTempDir(): string
{
return $s = $this->_getBuildDir() . '/' . $this->_aPrjConfig["fileprefix"] . "_" . date("Ymd-His");
}
/**
* Get full path where the project builds are (a build setes a subdir)
* @return string
*/
protected function _getBuildDir(): string
{
return $this->_aConfig['buildDir'] . '/' . $this->_aConfig["id"];
}
/**
* Get full path where the project default files are
* This is an optional step in build() - you can sync default files into
* the build directory. It returns false if the directory with default
* files doesn't exist.
*
* @return string|bool
*/
protected function _getDefaultsDir(): string|bool
{
$s = $this->_aConfig['buildDefaultsDir'] . '/' . $this->_aConfig["id"];
return file_exists($s) ? $s : false;
}
/**
* Get directory for infofile and package (without extension)
*
* @param string $sPhase one of preview|stage|live ...
* @param string $sPlace one of onhold|ready2install|deployed
* @return string
*/
protected function _getFileBase(string $sPhase, string $sPlace): string
{
if (!isset($this->_aConfig["phases"][$sPhase])) {
die(sprintf(t("class-project-error-wrong-phase"), $sPhase));
}
if (!isset($this->_aPlaces[$sPlace])) {
die(sprintf(t("class-project-error-wrong-place"), $sPlace));
}
// local file for onhold|ready2install
$sBase = $this->_aConfig['packageDir'] . "/" . $sPhase . "/" . $this->_aPrjConfig["fileprefix"];
if (!file_exists($this->_aConfig['packageDir'] . "/" . $sPhase)) {
mkdir($this->_aConfig['packageDir'] . "/" . $sPhase);
}
if ($sPlace == "onhold") {
$sBase .= "_onhold";
}
// $sBase .= "/" . $this->_aPrjConfig["fileprefix"];
// url for deployed
if ($sPlace == "deployed") {
if ($this->isActivePhase($sPhase) && isset($this->_aPrjConfig["phases"][$sPhase]["url"])) {
// $sBase = $this->_aPrjConfig["phases"][$sPhase]["url"] . $this->_aPrjConfig["fileprefix"];
$sBase = $this->_aPrjConfig["phases"][$sPhase]["url"];
} else {
$sBase = '';
}
}
// echo "DEBUG: ".__METHOD__."($sPhase, $sPlace) --> $sBase<br>";
return $sBase;
}
/**
* Get filename for info/ meta file (.json file).
* It returns false if the base directory doesn't exist
*
* @param string $sPhase one of preview|stage|live ...
* @param string $sPlace one of onhold|ready2install|deployed
* @return string
*/
protected function _getInfofile(string $sPhase, string $sPlace): string|bool
{
$sBase = $this->_getFileBase($sPhase, $sPlace);
return $sBase ? $sBase . '/' . $this->_aPrjConfig["fileprefix"] . '.json' : false;
}
/**
* Get filename for package file (without file extension)
* It returns false if the base directory doesn't exist
*
* @param string $sPhase one of preview|stage|live ...
* @param string $sPlace one of onhold|ready2install|deployed
* @return string
*/
protected function _getPackagefile(string $sPhase, string $sPlace)
{
$sBase = $this->_getFileBase($sPhase, $sPlace);
return $sBase ? $sBase . '/' . $this->_aPrjConfig["fileprefix"] : false;
}
/**
* Get a list of files of a given phase and place.
* It returns false if the base directory doesn't exist
*
* @param string $sBase base directory from where to get files (archive dir of a build)
* @return bool|array
*/
public function _getBuildfilesByDir(string $sBase): bool|array
{
$aReturn = [];
if (!$sBase || !is_dir($sBase)) {
return false;
}
$iTotalSize = 0;
$aReturn = [
'dir' => $sBase,
'filecount' => false,
'totalsize' => false,
'totalsize-hr' => false,
];
foreach (glob($sBase . '/*') as $sFile) {
$sFileBase = basename($sFile);
$sExt = pathinfo($sFile, PATHINFO_EXTENSION);
$aStat = stat($sFile);
switch ($sExt) {
case 'erb':
$sType = 'templates';
$sIcon = 'file-template';
break;
case 'tgz':
case 'zip':
$sType = 'package';
$sIcon = 'file-archive';
break;
case 'json':
$sType = 'metadata';
$sIcon = 'file-meta';
break;
default:
$sType = 'any';
$sIcon = 'file-any';
break;
}
$iTotalSize += $aStat['size'];
$aReturn['files'][$sFileBase] = [
'type' => $sType,
'icon' => $this->_oHtml->getIcon($sIcon),
'extension' => $sExt,
'size' => $aStat['size'],
];
$aReturn['types'][$sType][] = $sFileBase;
}
$aReturn['totalsize'] = $iTotalSize;
$aReturn['totalsize-hr'] = (round($iTotalSize / 1024 / 102.4) / 10) . ' MB';
$aReturn['filecount'] = count($aReturn['files']);
return $aReturn;
}
/**
* Get a list of files of a given phase and place;
* It returns false if the base directory for phase + base doesn't exist
*
* @param string $sPhase one of preview|stage|live ...
* @param string $sPlace one of onhold|ready2install|deployed
* @return bool|array
*/
public function getBuildfilesByPlace(string $sPhase, string $sPlace): bool|array
{
$sBase = $this->_getFileBase($sPhase, $sPlace);
return $this->_getBuildfilesByDir($sBase);
}
/**
* Get a list of files of a given version number
* It returns false if the version directory doesn't exist
*
* @param string $sVersion name of version
* @return array
*/
public function getBuildfilesByVersion(string $sVersion): bool|array
{
return $this->_getBuildfilesByDir($this->_getProjectArchiveDir() . '/' . $sVersion);
}
/**
* Get the group id of the project
* It returns false if the group wasn't set
*
* @return bool|string
*/
public function getProjectGroup(): bool|string
{
return isset($this->_aPrjConfig["projectgroup"]) && $this->_aPrjConfig["projectgroup"] != '-1' ? $this->_aPrjConfig["projectgroup"] : false;
}
/**
* Get the group label (description) of the project
* It returns false if the group wasn't set
*
* @return bool|string
*/
public function getProjectGroupLabel(): bool|string
{
$sGroupid = $this->getProjectGroup();
return isset($this->_aConfig["projectgroups"][$sGroupid]) ? $this->_aConfig["projectgroups"][$sGroupid] : false;
}
/**
* Get full path of a packed project archive
*
* @param string $sVersion version number of the build
* @return string
*/
protected function _getArchiveDir(string $sVersion): string
{
if (!$sVersion) {
die(t("class-project-error-_getArchiveDir-requires-id"));
}
return $this->_getProjectArchiveDir() . '/' . $sVersion;
}
/**
* get array of metadata of a given version. it returns
* - key "ok" anddata
* or
* - key "error" with the message
*
* @param string $sTimestamp
* @return array
*/
protected function _getArchiveInfos(string $sTimestamp): array
{
if (!$sTimestamp) {
die(t("class-project-error-_getArchiveInfos-requires-id"));
}
$sInfoFile = $this->_getArchiveDir($sTimestamp) . '/' . $this->_aPrjConfig["fileprefix"] . '.json';
$aReturn['infofile'] = $sInfoFile;
$sPackageFile = $this->_getArchiveDir($sTimestamp) . '/' . $this->_aPrjConfig["fileprefix"] . '.tgz';
$aReturn['packagefile'] = $sPackageFile;
if (!file_exists($sInfoFile)) {
$aReturn['error'] = sprintf(t("class-project-error-metafile-does-not-exist"), $sInfoFile);
return $aReturn;
}
$aJson = json_decode(file_get_contents($sInfoFile), true);
if (is_array($aJson) && isset($aJson["version"])) {
$aReturn = array_merge($aReturn, $aJson);
$aReturn['ok'] = 1;
/*
if (!file_exists($sPackageFile)) {
$aReturn['error'] = sprintf(t("class-project-error-datafile-does-not-exist"), $sInfoFile);
} else {
$aReturn['filesizes']['packagefile']=filesize($sPackageFile);
}
*
*/
return $aReturn;
}
$aReturn['error'] = sprintf(t("class-project-error-metafile-wrong-format"), $sInfoFile);
return $aReturn;
}
/**
* Get the directory for archive files of this project
*
* @return string
*/
public function _getProjectArchiveDir(): string
{
return $this->_aConfig["archiveDir"] . '/' . $this->_aConfig["id"];
}
/**
* Get all existing versions in archive and its usage
* versions are array keys; where they are used is written in values
*
* @return array
*/
public function getVersions(): array
{
// --- read all file entries
$aReturn = [];
$sDir = $this->_getProjectArchiveDir();
if (is_dir($sDir)) {
foreach (scandir($sDir) as $sEntry) {
if (is_dir($sDir . '/' . $sEntry) && $sEntry != '.' && $sEntry != '..')
$aReturn[$sEntry] = false;
}
}
$this->_aVersions = $aReturn;
// --- check version in all phases
$this->getAllPhaseInfos();
foreach ($this->_aData["phases"] as $sPhase => $aData) {
foreach (array_keys($this->_aPlaces) as $sPlace) {
if (isset($aData[$sPlace]["version"])) {
$this->_aVersions[$aData[$sPlace]["version"]][] = ['phase' => $sPhase, 'place' => $sPlace];
}
}
}
ksort($this->_aVersions);
return $this->_aVersions;
}
/**
* Get an array with all existing build error output files (html)
*
* @return array
*/
public function getBuildErrors(string $sProject = ''): array
{
// --- read all file entries
$aReturn = [];
if (!$sProject) {
$sProject = $this->_aPrjConfig["fileprefix"] . '_*';
}
foreach (glob($this->_getBuildDir() . '/' . $sProject . "/_output.html") as $sBuildDir) {
$aReturn[] = basename(dirname($sBuildDir)) . '/' . basename($sBuildDir);
}
return $aReturn;
}
/**
* Get an array with all existing build error output files (html)
* It returns false when path of given logfile contains ".." or the logfile doesn't exist.
*
* @return bool|string
*/
public function getBuildErrorContent($sLogfile): bool|string
{
if (!strpos('..', $sLogfile) === false) {
return false;
}
$sFilename = $this->_getBuildDir() . '/' . $sLogfile;
if (file_exists($sFilename)) {
return file_get_contents($sFilename);
}
return false;
}
/**
* Get an array of all versions, metainfos, in which phases they are in use
* and a rollback ist possible or not
*
* return array
*/
protected function _getVersionUsage(): array
{
$aVersionData = [];
$sLastVersion = false;
if (!count($this->getVersions())) {
return [];
}
foreach ($this->getVersions() as $sVersion => $aData) {
$aVersionData[$sVersion]["info"] = $this->_getArchiveInfos($sVersion);
foreach ($this->getActivePhases() as $sPhase) {
$bCanRollback = false;
foreach (array_keys($this->_aPlaces) as $sPlace) {
$bFound = false;
if (is_array($aData) && count($aData)) {
foreach ($aData as $i => $aPhaseUsage) {
if (
$aPhaseUsage["phase"] == $sPhase && $aPhaseUsage["place"] == $sPlace
)
$bFound = true;
}
}
$aVersionData[$sVersion]["usage"][$sPhase][$sPlace] = $bFound;
if ($bFound) {
$bCanRollback = false;
} else {
$bCanRollback = true;
if (
$sLastVersion && !$aVersionData[$sLastVersion]["rollback"][$sPhase]
) {
$bCanRollback = false;
}
/*
if (!isset($aVersionData[$sVersion]["info"]["ok"])){
$bCanRollback = false;
}
*/
}
$aVersionData[$sVersion]["rollback"][$sPhase] = $bCanRollback;
}
}
$sLastVersion = $sVersion;
}
return $aVersionData;
}
/**
* Recursive delete of a given directory
*
* @param string $dir directory to delete
* @return bool
*/
protected function _rmdir(string $dir): bool
{
foreach (scandir($dir) as $sEntry) {
if (is_dir($dir . '/' . $sEntry) && $sEntry != '.' && $sEntry != '..') {
$this->_rmdir($dir . '/' . $sEntry);
} elseif (is_file($dir . '/' . $sEntry) || is_link($dir . '/' . $sEntry))
unlink($dir . '/' . $sEntry);
}
return rmdir($dir);
}
/**
* Cleanup of archive directory; it returns the list of deleted
* directories as array.
* It returns a string with the error message in case of missing permission
* Otherwise it returns an array with all deleted directories.
*
* @param bool $bDeleteAll flag to delete all; default: false = it keeps a few versions
* @return string|array
*/
public function cleanupArchive(bool $bDeleteAll = false): string|array
{
if (!$this->oUser->hasPermission("project-action-cleanup")) {
return $this->oUser->showDenied();
}
$aDelete = [];
$aUnused = [];
$sDir = $this->_getProjectArchiveDir();
$this->getVersions();
if (!$this->_aVersions) {
return $aDelete;
}
// find unused versions
foreach ($this->_aVersions as $sVersion => $aUsage) {
if (!$aUsage || count($aUsage) == 0) {
$aUnused[] = $sVersion;
}
}
// keep a few
$iKeep = $bDeleteAll ? 0 : $this->_aConfig["versionsToKeep"];
while (count($aUnused) && count($aUnused) > $iKeep) {
$sVersion = array_shift($aUnused);
$sDir2 = $sDir . '/' . $sVersion;
if (is_dir($sDir2)) {
if ($this->_rmdir($sDir2)) {
$aDelete[] = $sDir2;
echo t('ok') . ': ' . $sDir2;
} else {
echo sprintf(t("class-project-warning-cannot-delete-archive-dir"), $sDir2);
}
} else {
echo t('skip') . ': ' . $sDir2;
}
echo '<br>';
}
// rescan versions
if (count($aDelete)) {
$this->getVersions();
}
return $aDelete;
}
/**
* Cleanup of kept build directories (builds with errors) except the last N builds;
* It returns a string with the error message in case of missing permission
* Otherwise it returns an array with all deleted directories.
*
* @return string|array
*/
public function cleanupBuilds(): string|array
{
$this->log(__FUNCTION__ . " start");
if (!$this->oUser->hasPermission("project-action-cleanup")) {
return $this->oUser->showDenied();
}
$sDir = $this->_getBuildDir();
$aDirlist = [];
$aDelete = [];
if (is_dir($sDir)) {
foreach (scandir($sDir) as $sEntry) {
if (is_dir($sDir . '/' . $sEntry) && $sEntry != '.' && $sEntry != '..')
$aDirlist[] = $sEntry;
}
}
// keep a few
while (count($aDirlist) > $this->_aConfig["builtsToKeep"]) {
$sVersion = array_shift($aDirlist);
$sDir2 = $sDir . '/' . $sVersion;
if ($this->_rmdir($sDir2)) {
$aDelete[] = $sDir2;
} else {
echo sprintf(t("class-project-warning-cannot-delete-build-dir"), $sDir2);
}
;
}
return $aDelete;
}
/**
* Cleanup cache of vcs
* It returns a string with error message in case of missing permission
* Otherwise it returns the success as true or false
*
* @param int $iAge max age in sec
* @return string|bool
*/
public function cleanupVcsCache($iAge = 0): string|bool
{
$this->log(__FUNCTION__ . " start");
if (!$this->oUser->hasPermission("project-action-cleanup")) {
return $this->oUser->showDenied();
}
$this->_initVcs();
if (isset($this->_oVcs) && $this->_oVcs) {
if (!method_exists($this->_oVcs, "cleanupCache")) {
// the version control class does not have this method
$this->log(__FUNCTION__ . " sorry, Method cleanupCache does not exist in this VCS class.");
return false;
}
return $this->_oVcs->cleanupCache($iAge);
}
return false;
}
/**
* Get conmplete config of the project
*
* @return array
*/
public function getConfig(): array
{
return $this->_aPrjConfig;
}
/**
* Get name/ label of the project
*
* @return string
*/
public function getLabel(): string
{
return isset($this->_aPrjConfig["label"]) ? $this->_aPrjConfig["label"] : '';
}
/**
* Get description of the project
*
* @return string
*/
public function getDescription(): string
{
return isset($this->_aPrjConfig["description"]) ? $this->_aPrjConfig["description"] : '';
}
/**
* Get the id of the current project
* @return string
*/
public function getId(): string
{
return isset($this->_aConfig["id"]) ? $this->_aConfig["id"] : '';
}
/**
* Get deploy and queue infos for all phases
* It build up a subkey "progress" with info if a build is queued
* or an installation of a new package is going on
*
* @return array
*/
public function getAllPhaseInfos(): array
{
$bHasQueue = false;
$bHasDifferentVersions = false;
$bFirstVersion = false;
if (!isset($this->_aData["phases"])) {
$this->_aData["phases"] = [];
}
if (!isset($this->_aData["progress"])) {
$this->_aData["progress"] = [];
}
foreach (array_keys($this->_aConfig["phases"]) as $sPhase) {
if (!isset($this->_aData["phases"][$sPhase])) {
$this->getPhaseInfos($sPhase);
}
// detect progress
$aDataPhase = $this->_aData["phases"][$sPhase];
foreach (array_keys($this->getPlaces()) as $sPlace) {
if (
$sPlace !== 'onhold'
&& isset($aDataPhase[$sPlace]['version'])
) {
if ($bFirstVersion && !$bHasDifferentVersions && $bFirstVersion !== $aDataPhase[$sPlace]['version']) {
$bHasDifferentVersions = true;
}
if (!$bFirstVersion) {
$bFirstVersion = $aDataPhase[$sPlace]['version'];
}
}
}
// check queue
if (!$bHasQueue && isset($aDataPhase['onhold']['version']) && $aDataPhase['onhold']['version']) {
$bHasQueue = true;
}
}
$this->_aData["progress"] = [
'inprogress' => $bHasDifferentVersions,
'hasQueue' => $bHasQueue,
];
return $this->_aData["phases"];
}
/**
* Get statusinfos of a named phase
*
* @param string $sPhase name of the phase; one of preview|stage|live
* @return array
*/
public function getPhaseInfos(string $sPhase): array
{
if (!$sPhase) {
die(t("class-project-error-getPhaseInfos-requires-phase"));
}
if (!isset($this->_aData["phases"]))
$this->_aData["phases"] = [];
if (!isset($this->_aData["phases"][$sPhase])) {
if ($this->isActivePhase($sPhase)) {
$this->_aData["phases"][$sPhase] = [];
$aTmp = [];
// a blocked package is waiting for deployment timeslot?
$sKey = "onhold";
$sJsonfile = $this->_getInfofile($sPhase, $sKey);
$aTmp[$sKey] = [];
if (file_exists($sJsonfile)) {
$aJson = json_decode(file_get_contents($sJsonfile), true);
if (isset($aJson["version"])) {
$aTmp[$sKey] = $aJson;
$aTmp[$sKey]["infofile"] = $sJsonfile;
$aTmp[$sKey]["ok"] = 1;
} else {
$aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-has-no-version"), $sJsonfile, print_r($aJson, true));
}
} else {
$aTmp[$sKey]["info"] = t("class-project-info-no-package-in-queue");
$aTmp[$sKey]["ok"] = 1;
}
// package for puppet
$sKey = "ready2install";
$sJsonfile = $this->_getInfofile($sPhase, $sKey);
$aTmp[$sKey] = [];
if (file_exists($sJsonfile)) {
// $sPkgfile = $this->_getPackagefile($sPhase, $sKey);
// if (file_exists($sPkgfile)) {
$aJson = json_decode(file_get_contents($sJsonfile), true);
if (isset($aJson["version"])) {
$aTmp[$sKey] = $aJson;
$aTmp[$sKey]["infofile"] = $sJsonfile;
// $aTmp[$sKey]["packagefile"] = $sPkgfile;
$aTmp[$sKey]["ok"] = 1;
} else {
$aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-has-no-version"), $sJsonfile, print_r($aJson, true));
}
// } else {
// $aTmp[$sKey]["error"] = sprintf(t("class-project-error-getPhaseInfos-package-not-found"), $sPkgfile);
// }
} else {
$aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-does-not-exist"), $sJsonfile);
}
// published data
$sKey = "deployed";
$sJsonfile = $this->_getInfofile($sPhase, $sKey);
$aTmp[$sKey] = [];
// use version cache
require_once(__DIR__ . '/../../valuestore/classes/valuestore.class.php');
$oVersion = new valuestore();
$oVersion->setProject("", $this->_aPrjConfig["fileprefix"], $sPhase, $sKey);
$aVersions = $oVersion->getVersion();
// echo "Place: <pre>" . print_r($oVersion->whereiam(), 1) . "</pre>";
// echo "Versionen: <pre>" . print_r($aVersions, 1) . "</pre>";
if (count($aVersions)) {
$aTmp[$sKey] = [];
$aTmp[$sKey] = $aVersions[0]['_data'];
$aTmp[$sKey]["infofile"] = '[versioncache]';
$aTmp[$sKey]['_hosts'] = [];
foreach ($aVersions as $sHostname => $aHostdata) {
$aTmp[$sKey]['_hosts'][$aHostdata['host']] = $aHostdata;
}
$aTmp[$sKey]["ok"] = 1;
$aTmp[$sKey]["infofile"] = '[versioncache]';
}
/*
$sJsonData = $this->_httpGet($sJsonfile);
if ($sJsonData) {
$aJson = json_decode($sJsonData, true);
if (is_array($aJson) && array_key_exists("version", $aJson)) {
$aTmp[$sKey] = $aJson;
$aTmp[$sKey]["infofile"] = $sJsonfile;
$aTmp[$sKey]["ok"] = 1;
} else {
$aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-has-no-version"), $sJsonfile, print_r($aJson, true));
}
} else {
$aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-wrong-format"), $sJsonfile);
}
*
*/
} else {
$aTmp['onhold']["warning"] = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
$aTmp['ready2install']["warning"] = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
$aTmp['deployed']["warning"] = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
}
$this->_aData["phases"][$sPhase] = $aTmp;
}
// echo '<pre>'.print_r($this->_aData["phases"][$sPhase], 1).'</pre>'.__METHOD__.'<br>';
return $this->_aData["phases"][$sPhase];
}
/**
* get a list of all existing projects as a flat array;
* it can be ordered by "id" or "label"
* <code>
* print_r($oPrj->getProjects("label"));
* </code>
* returns<br>
* Array ( [0] => project1 [1] => project2 )
*
* @param string $sort sort by "id" (default) or "label"
* @return array
*/
public function getProjects(string $sort = 'id'): array
{
$aReturn = [];
foreach (glob(dirname($this->_getConfigFile("dummy")) . "/*.json") as $filename) {
$aReturn[] = str_replace(".json", "", basename($filename));
}
if ($sort == "label") {
$aProjectLabels = [];
foreach ($aReturn as $sPrj) {
$oPrj = new project($sPrj);
$aProjectLabels[strtolower($oPrj->getLabel() . '-' . $sPrj)] = [
'id' => $sPrj,
'label' => $oPrj->getLabel(),
'description' => $oPrj->getDescription(),
];
}
ksort($aProjectLabels);
return array_values($aProjectLabels);
}
if ($sort == "id") {
sort($aReturn);
}
return $aReturn;
}
/**
* Check if the given phase is active for this project
*
* @param string $sPhase name of the phase; one of preview|stage|live
* @return bool
*/
public function isActivePhase(string $sPhase): bool
{
return (
$this->_aPrjConfig && isset($this->_aPrjConfig["phases"][$sPhase]["active"][0])
? $this->_aPrjConfig["phases"][$sPhase]["active"][0]
: false
);
}
/**
* Get array of all (active and inactive) phases
* @return array
*/
public function getPhases(): array
{
return $this->_aConfig["phases"];
}
/**
* Get array of all (active and inactive) phases
* @return array
*/
public function getPlaces(): array
{
return $this->_aPlaces;
}
/**
* Get a flat array with active phases of the project
* @return array
*/
public function getActivePhases(): array
{
$aReturn = [];
foreach (array_keys($this->_aConfig["phases"]) as $s) {
if ($this->isActivePhase($s)) {
$aReturn[] = $s;
}
}
return $aReturn;
}
/**
* find the next active phase of a project
* @param string $sPhase current phase; if empty the function sends back the first phase
* @return string
*/
public function getNextPhase(string $sPhase = ''): string
{
if ($sPhase) {
if (!isset($this->_aConfig["phases"][$sPhase])) {
die(sprintf(t("class-project-error-wrong-phase"), $sPhase));
}
}
$sNextPhase = false;
$bUseNextPhase = $sPhase ? false : true;
foreach (array_keys($this->_aConfig["phases"]) as $s) {
if ($bUseNextPhase) {
if ($this->isActivePhase($s)) {
$sNextPhase = $s;
$bUseNextPhase = false;
continue;
}
}
if ($sPhase == $s) {
$bUseNextPhase = true;
}
}
return $sNextPhase;
}
/**
* Get an array with deploy status ...
* 'inprogress'=>do versions differ from phase to phase = rollout of a version is in progress
'hasQueue'=>is there a package in a queue (waiting for deployment time to get ready to be installed)
* @return array
*/
public function getProgress(): array
{
$this->getAllPhaseInfos();
return $this->_aData['progress'];
}
/**
* check: is the deployment to the next phase enabled for this phase?
* It returns a string when current user has no permissions.
* Otherwise it returns true or false.
*
* @param string $sPhase current phase
* @return string|bool
*/
public function canAcceptPhase(string $sPhase = ''): string|bool
{
if (
!$this->oUser->hasPermission("project-action-accept") && !$this->oUser->hasPermission("project-action-accept-$sPhase")
) {
// echo $this->oUser->showDenied();
return '<span class="btn" title="no permission [project-action-accept] for user [' . $this->oUser->getUsername() . ']">' . $sPhase . '</span>';
}
if (!$sPhase) {
// for better performance: skip check on overview page
/*
$aRepodata = $this->getRepoRevision();
if (!isset($aRepodata["revision"])) {
return false;
}
*/
$sNext = $this->getNextPhase($sPhase);
return $sNext > '';
}
if (!isset($this->_aConfig["phases"][$sPhase])) {
die(sprintf(t("class-project-error-wrong-phase"), $sPhase));
}
if (!$this->isActivePhase($sPhase)) {
// die("ERROR: the phase $sPhase is not active in this project.");
return false;
}
$sNext = $this->getNextPhase($sPhase);
if (!$sNext) {
return false;
}
// ensure that _aData is filled
$this->getPhaseInfos($sPhase);
// array key "ok" must be in the ready2install and deployed info
// and a version must be installed
if (
isset($this->_aData["phases"][$sPhase])
&& isset($this->_aData["phases"][$sPhase]["onhold"])
&& isset($this->_aData["phases"][$sPhase]["ready2install"])
&& isset($this->_aData["phases"][$sPhase]["deployed"])
&& isset($this->_aData["phases"][$sPhase]["onhold"]["ok"])
&& isset($this->_aData["phases"][$sPhase]["ready2install"]["ok"])
&& isset($this->_aData["phases"][$sPhase]["deployed"]["ok"])
&& isset($this->_aData["phases"][$sPhase]["deployed"]["version"])
) {
return true;
}
return false;
}
/**
* Get list of remote branches and tags.
* It returns false if the VCS was not initialize or has no method getRemoteBranches()
*
* @param bool $bIgnoreCache flag to ignore exiting cached data
* @return bool|array
*/
public function getRemoteBranches(bool $bIgnoreCache = false): bool|array
{
$this->log(__FUNCTION__ . "($bIgnoreCache) start");
$this->_initVcs();
if (isset($this->_oVcs) && $this->_oVcs) {
if (!method_exists($this->_oVcs, "getRemoteBranches")) {
// the version control class does not have this method
return false;
}
return $this->_oVcs->getRemoteBranches($bIgnoreCache);
}
return false;
}
/**
* Get current revision and log message from remote repo
*
* @param boolean $bRefresh optional: refresh data; default: use cache
* @return array
*/
public function getRepoRevision(bool $bRefresh = false): array
{
$this->log(__FUNCTION__ . "($bRefresh) start");
if (!$this->_aPrjConfig["build"]["type"]) {
$this->_aData["phases"]["source"] = ["error" => t("class-project-error-repo-type-not-set"),];
} else {
$this->_initVcs();
if (isset($this->_oVcs) && $this->_oVcs) {
$this->_aData["phases"]["source"] = $this->_oVcs->getRepoRevision($bRefresh);
} else {
$this->_aData["phases"]["source"] = [
"error" => sprintf(t("class-project-error-repo-type-not-supported"), $this->_aPrjConfig["build"]["type"]),
];
}
}
$this->log(__FUNCTION__ . " result:<pre>" . print_r($this->_aData, 1) . "</pre>");
return $this->_aData["phases"]["source"];
}
/**
* Initialize version control system (git, ...) if it is not initialized yet
* it sets the object $this->_oVcs
*/
protected function _initVcs(): void
{
$this->log(__FUNCTION__ . " start");
if (!isset($this->_oVcs)) {
if (!$this->_aPrjConfig["build"]["type"]) {
$this->_aData["phases"]["source"] = ["error" => t("class-project-error-repo-type-not-set"),];
} else {
if (!@include_once("vcs." . $this->_aPrjConfig["build"]["type"] . ".class.php")) {
$this->_aData["phases"]["source"] = [
"error" => sprintf(t("class-project-error-repo-type-not-supported"), $this->_aPrjConfig["build"]["type"]),
];
} else {
$aConfig = $this->_aPrjConfig["build"];
// for vcs classes
$aConfig["appRootDir"] = $this->_aConfig["appRootDir"];
$aConfig["dataDir"] = $this->_aConfig["dataDir"];
$aConfig["tmpDir"] = $this->_aConfig["tmpDir"];
$this->_oVcs = new vcs($aConfig);
if ($this->_sBranchname) {
if (method_exists($this->_oVcs, "setCurrentBranch")) {
$this->_oVcs->setCurrentBranch($this->_sBranchname);
}
}
}
}
}
// return $this->_oVcs;
}
/**
* Get an array of enabled plugins
* @param string $sSection one of false|"rollout"|...
* @return array
*/
public function getConfiguredPlugins(string $sSection = ''): array
{
$aReturn = [];
if (!$sSection) {
$aReturn = $this->_aConfig["plugins"];
} else {
foreach ($this->_aConfig["plugins"][$sSection] as $sPluginName => $aItem) {
$aReturn[$sPluginName] = $aItem;
}
}
return $aReturn;
}
/**
* Get a location of a plugin file with full path
*
* @param string $sType type of plugin, i.e. "rollout"
* @param string $sPluginName Name of plugin
* @return string
*/
protected function _getPluginFilename(string $sType, string $sPluginName): string
{
return __DIR__ . '/../plugins/' . $sType . '/' . $sPluginName . '/' . $sType . '_' . $sPluginName . '.php';
}
/**
* Get a flat array of all existing ssh keys
* @return array
*/
protected function _getSshKeys(): array
{
$aReturn = [];
foreach (glob($this->_aConfig["dataDir"] . "/sshkeys/*.pub") as $filename) {
$aReturn[] = str_replace(".pub", "", basename($filename));
}
sort($aReturn);
return $aReturn;
}
/**
* Get a flat array with regexes of deploy times.
* It returns false if the given phase is not active
*
* @param string $sPhase phase
* @return bool|array
*/
protected function _getDeploytimes(string $sPhase): bool|array
{
if (!$this->isActivePhase($sPhase)) {
$sError = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
$this->_logaction($sError, __FUNCTION__, "error");
return false;
}
$aDeploytimes = $this->_aConfig["phases"][$sPhase]["deploytimes"] ?? [];
if (
isset($this->_aPrjConfig["phases"][$sPhase]["deploytimes"])
&& $this->_aPrjConfig["phases"][$sPhase]["deploytimes"]
) {
$aDeploytimes = [$this->_aPrjConfig["phases"][$sPhase]["deploytimes"]];
}
return $aDeploytimes;
}
// ----------------------------------------------------------------------
// SETTER
// ----------------------------------------------------------------------
/**
* Generate a filename for the process output.
* The last output file will be deleted if it exists.
* It returns false if no pram for new basename was given.
* Otherwise it returns a filename with full path in temp folder.
*
* @param string $sNewTempfile
* @return bool|string
*/
protected function _setProcessOutFile(string $sNewTempfile = ''): bool|string
{
if ($this->_sProcessTempOut && file_exists($this->_sProcessTempOut)) {
unlink($this->_sProcessTempOut);
}
// $sNewTempfile = sys_get_temp_dir() . "/" . basename($sNewTempfile);
$this->_sProcessTempOut = $sNewTempfile ? sys_get_temp_dir() . "/" . basename($sNewTempfile) : false;
return $this->_sProcessTempOut;
}
/**
* Get projects from ldap; it returns ldap search items with cn as
* array key.
* It returns false if no result was found
*
* @param string $sSearchFilter LDAP search filter
* @return bool|array
*/
protected function _ldapProjectSearch(string $sSearchFilter): bool|array
{
$aReturn = [];
require_once("ldap.class.php");
$oLdapIML = new imlldap($this->_aConfig['projects']['ldap']);
// $oLdapIML->debugOn();
$aResultsIml = $oLdapIML->searchDn(
$this->_aConfig['projects']['ldap']['DnProjects'],
$sSearchFilter,
["*"]
);
if (!$aResultsIml['count']) {
return false;
}
$oLdapIML->close();
/*
unset($aResultsIml['count']);
foreach ($aResultsIml as $aItem) {
$aReturn[$aItem['cn'][0]] = [
'dn' => $aItem['dn'],
'cn' => $aItem['cn'][0],
'_description' => $aItem['description'][0],
'title' => $sTitle,
'description' => $sDescription,
];
}
$oLdapIML->close();
ksort($aReturn);
return $aReturn;
*
*/
return $aResultsIml;
}
/**
* Load config of a project with given project id and turn a bool for success
*
* @param string $sId new project id of project to load
* @return boolean Success
*/
public function setProjectById(string $sId): bool
{
if ($sId !== preg_replace('/[^a-z0-9\-\_]/i', '', $sId)) {
$this->_errors[] = sprintf(t("class-project-error-config-wrongid"), htmlentities($sId));
return false;
}
$this->_aPrjConfig = [];
$this->_aConfig["id"] = $sId;
$this->_errors = [];
if (isset($this->_aConfig['projects']['json']['active']) && $this->_aConfig['projects']['json']['active']) {
$sCfgfile = $this->_getConfigFile($sId);
if (!$sCfgfile || !file_exists($sCfgfile)) {
return false;
}
$_aPrjConfigTmp = json_decode(file_get_contents($this->_getConfigFile($sId)), true);
if (!$_aPrjConfigTmp) {
$this->_errors[] = sprintf(t("class-project-error-config-invalid"), $sId, $this->_getConfigFile($sId));
return false;
}
$this->_aPrjConfig = $_aPrjConfigTmp;
}
if (isset($this->_aConfig['projects']['ldap']['active']) && $this->_aConfig['projects']['ldap']['active']) {
// TODO: read project after saving it - @see $this->saveConfig()
$sQuery = '(&(objectclass=hieraSource)(documentIdentifier=' . $sId . '))';
$aResult = $this->_ldapProjectSearch($sQuery);
// echo '<pre>$aResult = ' . print_r($aResult, 1) . '</pre>';
if (isset($aResult[0]['hieradata'])) {
foreach ($aResult[0]['hieradata'] as $sLine) {
// echo $sLine.'<br>';
if (preg_match('/^cfg=/', $sLine)) {
// echo $sLine.'<br>';
$this->_aPrjConfig = json_decode(preg_replace('/^cfg=/', '', $sLine), 1);
}
}
}
// return $this->objAdd($sDn, $aItem);
}
// $aData=json_decode(file_get_contents($this->_getConfigFile($sId)), true);
// echo "<pre>" . print_r($aData, true) . "</pre>";
$this->_verifyConfig();
// ----- init rollout plugin
// set name of the activated plugin for this project
$sPluginName = (isset($this->_aPrjConfig['deploy']['enabled_rollout_plugin']) && $this->_aPrjConfig['deploy']['enabled_rollout_plugin'])
? $this->_aPrjConfig['deploy']['enabled_rollout_plugin']
: 'default';
unset($this->oRolloutPlugin);
try {
require_once $this->_getPluginFilename('rollout', $sPluginName);
$sPluginClassname = 'rollout_' . $sPluginName;
$this->oRolloutPlugin = new $sPluginClassname([
'lang' => $this->_aConfig['lang'],
'phase' => false,
'globalcfg' => isset($this->_aConfig['plugins']['rollout'][$sPluginName]) ? $this->_aConfig['plugins']['rollout'][$sPluginName] : [],
'projectcfg' => $this->_aPrjConfig,
]);
// print_r($this->_oRolloutPlugin->getPluginfos());
// print_r($this->_oRolloutPlugin->getName());
} catch (Exception $ex) {
}
return true;
}
/**
* set a branchname
* @param string $sBranchname name of the branch, i.e. "origin/master"
* @return string
*/
public function setBranchname(string $sBranchname): string
{
$this->_sBranchname = $sBranchname;
if (isset($this->_oVcs) && $this->_oVcs) {
if (method_exists($this->_oVcs, "setCurrentBranch")) {
$this->_oVcs->setCurrentBranch($sBranchname);
}
}
return $this->_sBranchname;
}
// ----------------------------------------------------------------------
// ACTIONS
// ----------------------------------------------------------------------
/**
* Store data to a tempfile (read by for ajax polling) and update actions box
*
* @param string $sData full output of all so far executed shell commands
* @param array $aActions for right output box: Array of actions with marker of current action
* see build() method for the structure
* @return bool|int
*/
protected function _TempFill(string $sData, array $aActions = []): bool|int
{
if (!$this->_sProcessTempOut) {
return false;
}
$sActions = '';
if (count($aActions)) {
for ($i = 0; $i < count($aActions["actions"]); $i++) {
$sActions .= '<li';
if ($i == $aActions["iActive"]) {
$sActions .= ' class="active"';
}
$sActions .= '>' . $aActions["actions"][$i]['label'] . '</li>';
}
if ($sActions) {
$sData = '<div style="float: right; background: #f8f8f8; padding: 1em;">'
. '<strong>' . $aActions["label"] . '</strong>'
. '<ol class="actions">'
. $sActions
. '</ol></div>'
. $sData;
}
}
return file_put_contents($this->_sProcessTempOut, $sData);
}
/**
* Delete tempfile for ajax polling; if a directory is given as parameter
* the tmp file will be moved there.
*
* @param string $sTempDir optional; target dir to copy; default=false (=delete file)
* @return boolean
*/
protected function _TempDelete(string $sTempDir = ''): bool
{
if (!$this->_sProcessTempOut) {
return false;
}
if (file_exists($this->_sProcessTempOut)) {
if ($sTempDir && is_dir($sTempDir)) {
$sKeepOutfile = $sTempDir . '/_output.html';
copy($this->_sProcessTempOut, $sKeepOutfile);
}
unlink($this->_sProcessTempOut);
}
return file_exists($this->_sProcessTempOut);
}
/**
* Get the name of the current branch (or default branch).
* It returns false if the vcs was not initialized yet.
*
* @return string|bool
*/
public function getBranchname(): string|bool
{
$this->log(__FUNCTION__ . " start");
$this->_initVcs();
if (isset($this->_oVcs) && $this->_oVcs) {
if (method_exists($this->_oVcs, "getCurrentBranch")) {
$sCurrentBranch = $this->_oVcs->getCurrentBranch(true); // true means search for master branch if empty
if ($sCurrentBranch) {
$this->setBranchname($sCurrentBranch);
return $sCurrentBranch;
}
}
}
return false;
}
/**
* Build a new package for the deployment. It will be put to the queue
* of the first active phase (i.e. preview).
* If there is no deployment time range it will be deployed too.
*
* @global string $sTmpFile
* @return boolean|string false or HTML code
*/
public function build(string $sTmpFile = ''): bool|string
{
$this->log(__FUNCTION__ . " start");
if (!$this->oUser->hasPermission("project-action-build")) {
return $this->oUser->showDenied();
}
global $aParams;
$sReturn = false;
$aActionList = [
'iActive' => 0,
'label' => t('build'),
'actions' => [
['label' => t('class-project-build-label-cleanup-builds')],
['label' => t('class-project-build-label-create-workdir')],
['label' => t('class-project-build-label-get-sources-from-version-control')],
['label' => t('class-project-build-label-execute-hook-postclone')],
['label' => t('class-project-build-label-copy-default-structure')],
['label' => t('class-project-build-label-execute-hook-precompress')],
['label' => t('class-project-build-label-cleanup-project')],
['label' => t('class-project-build-label-create-package')],
['label' => t('class-project-build-label-remove-workdir')],
['label' => t('class-project-build-label-queue-to-first-active-phase')],
],
];
$this->_setProcessOutFile($sTmpFile);
$this->_iRcAll = 0;
// return $this->_execAndSend("bash --login -c 'ruby --version' " . $sTempDir);
$this->_logaction(t('starting') . " build()", __FUNCTION__);
$sReturn = "<h2>" . t("build") . " " . $this->getLabel() . "</h2>";
// --------------------------------------------------
// cleanup
// --------------------------------------------------
$aDirs = $this->cleanupBuilds();
if (count($aDirs)) {
$sReturn .= '<h3>' . t('class-project-build-label-cleanup-builds') . '</h3><pre>' . print_r($aDirs, true) . '</pre>';
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
$this->_initVcs();
if (!isset($this->_oVcs) || !$this->_oVcs) {
$sError = sprintf(t('class-project-error-build-type-not-supported'), $this->_aPrjConfig["build"]["type"]);
$this->_logaction($sError, __FUNCTION__, "error");
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
// --------------------------------------------------
// create workdir
// --------------------------------------------------
$sTempBuildDir = $this->_getTempDir();
$sFirstLevel = $this->getNextPhase();
if (!$sFirstLevel) {
$this->_TempDelete();
$this->_logaction(t('page-overview-no-phase'), __FUNCTION__, "error");
return false;
}
$sReturn .= '<h3>' . t('class-project-build-label-create-workdir') . '</h3>';
if (!file_exists($sTempBuildDir)) {
$sReturn .= $this->_execAndSend("mkdir -p " . $sTempBuildDir);
}
$sReturn .= $this->_execAndSend("ls -ld " . $sTempBuildDir);
if (!file_exists($sTempBuildDir)) {
$this->_TempDelete();
$sError = sprintf(t('"class-project-error-build-dir-was-not-created"'), $sTempBuildDir);
$this->_logaction($sError, __FUNCTION__, "error");
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
// $this->_iRcAll = 0;
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// checkout
// --------------------------------------------------
$sReturn .= '<h3>' . t('class-project-build-label-get-sources-from-version-control') . '</h3>';
$sCheckout=$this->_oVcs->getSources($sTempBuildDir);
$sReturn .= '<pre>' . $sCheckout . '</pre>';
// fetch last line "rc=NNN"
preg_match("/rc=([0-9]+)$/", $sCheckout, $aMatches);
$iRc=$aMatches[1] ?? 0;
if ($iRc != 0) {
return $this->_oHtml->getBox("error", "" . $sReturn);
}
$aRepodata = $this->getRepoRevision();
$sRevisionShort = substr($aRepodata['revision'], 0, 8);
$sReturn .= $this->_oHtml->getBox("info", t('commitmessage') . '<pre>' . htmlentities($aRepodata['message']) . '</pre>');
$sReturn .= $this->_execAndSend("ls -lisa $sTempBuildDir");
if (!$this->_iRcAll == 0) {
$sError = sprintf(t('class-project-error-command-failed'), $sTempBuildDir);
$this->_logaction($sError, __FUNCTION__, "error");
$this->_TempFill($sError . $sReturn, $aActionList);
$this->_TempDelete($sTempBuildDir);
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
// --------------------------------------------------
foreach (glob($sTempBuildDir . '/hooks/on*') as $filename) {
$sReturn .= 'chmod 755 ' . $filename . '<br>';
$sReturn .= $this->_execAndSend('chmod 755 ' . $filename);
$sReturn .= $this->_execAndSend('ls -l ' . $filename);
}
// --------------------------------------------------
$sCfgout = $sTempBuildDir . '/ci-custom-vars';
$sCfgContent = '';
if (isset($this->_aPrjConfig['deploy']["configfile"]) && $this->_aPrjConfig['deploy']["configfile"]) {
# task#5047 - FIX EOL
$sCfgContent .= $this->_aPrjConfig['deploy']["configfile"];
// detect unix, linux, mac
if (DIRECTORY_SEPARATOR === '/') {
$sCfgContent = str_replace("\r\n", "\n", $sCfgContent);
}
# /task#5047
}
$aCivars = [
'branch' => $aRepodata['branch'],
'branch_short' => $aRepodata['shortname'],
'branch_type' => $aRepodata['type'],
'revision' => $aRepodata['revision'],
'revision_short' => $sRevisionShort,
'imagepart' => $aRepodata['type'] == 'tags'
? $aRepodata['shortname']
: $sRevisionShort
,
// 'message'=>$aRepodata['message'],
];
$sCfgContent .= "\n"
. "# ---------- generated CI SERVER variables\n"
. "\n"
;
foreach ($aCivars as $sKey => $value) {
$sCfgContent .= "export CI_$sKey=\"$value\"; \n";
}
file_put_contents($sCfgout, $sCfgContent);
$sReturn .= $this->_execAndSend('ls -l ' . $sCfgout);
$sReturn .= $this->_execAndSend('cat ' . $sCfgout);
$sReturn .= $this->_oHtml->getBox("success", t('class-project-info-build-checkout-ok'));
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// execute hook postclone
// --------------------------------------------------
// task#1726 - add environment
$sSetEnv = ''
. 'export GIT_SSH="' . $this->_aConfig['appRootDir'] . '/shellscripts/gitsshwrapper.sh";'
. 'export DIR_SSH_KEYS="' . $this->_aConfig['dataDir'] . '/sshkeys";'
. 'export DIR_APPROOT="' . $sTempBuildDir . '";'
. 'export NVMINIT="' . $this->_aConfig['appRootDir'] . '/shellscripts/nvm_init.sh";'
. (isset($this->_aConfig['build']['env']) ? $this->_aConfig['build']['env'] : '');
$sHookfile = $this->_aConfig['build']['hooks']['build-postclone'];
$sReturn .= '<h3>' . t('class-project-build-label-execute-hook-postclone') . ' (' . $sHookfile . ')</h3>';
if (file_exists($sTempBuildDir . '/' . $sHookfile)) {
// $sReturn.=$this->_execAndSend('chmod 755 ' . $sTempDir . '/hooks/on*');
// $this->_iRcAll = 0;
$sReturn .= $this->_execAndSend('bash --login -c \'' . $sSetEnv . ' ' . $sTempBuildDir . '/' . $sHookfile . '\'');
if (!$this->_iRcAll == 0) {
$sError = sprintf(t('class-project-error-command-failed'), $sTempBuildDir);
$this->_logaction($sError, __FUNCTION__, "error");
$this->_TempFill($sError . $sReturn, $aActionList);
$this->_TempDelete($sTempBuildDir);
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
} else {
$sReturn .= t('skip') . '<br>';
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// copy default structure
// --------------------------------------------------
$sReturn .= '<h3>' . t('class-project-build-label-copy-default-structure') . '</h3>';
if ($this->_getDefaultsDir()) {
$sReturn .= $this->_execAndSend("find " . $this->_getDefaultsDir() . " | head -15");
$sReturn .= $this->_execAndSend("rsync -r " . $this->_getDefaultsDir() . "/* $sTempBuildDir");
// $sReturn.=$this->_execAndSend("find $sTempDir");
} else {
$sReturn .= t('skip') . '<br>';
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// execute hook
// --------------------------------------------------
$sHookfile = $this->_aConfig['build']['hooks']['build-precompress'];
$sReturn .= '<h3>' . t('class-project-build-label-execute-hook-precompress') . ' (' . $sHookfile . ')</h3>';
if (file_exists($sTempBuildDir . '/' . $sHookfile)) {
// $sReturn.=$this->_execAndSend('chmod 755 ' . $sTempDir . '/hooks/on*');
// $this->_iRcAll = 0;
$sReturn .= $this->_execAndSend('bash --login -c \'' . $sSetEnv . ' ' . $sTempBuildDir . '/' . $sHookfile . '\'');
if (!$this->_iRcAll == 0) {
$sError = sprintf(t('class-project-error-command-failed'), $sTempBuildDir);
$this->_logaction($sError, __FUNCTION__, "error");
$this->_TempFill($sError . $sReturn, $aActionList);
$this->_TempDelete($sTempBuildDir);
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
} else {
$sReturn .= t('skip') . '<br>';
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// cleanup .git, .svn, ...
// --------------------------------------------------
$sReturn .= '<h3>' . t('class-project-build-label-cleanup-project') . '</h3>';
if (isset($this->_oVcs) && $this->_oVcs) {
$this->_oVcs->cleanupWorkdir($sTempBuildDir);
}
// $sReturn.=$this->_execAndSend("cd $sTempDir && rm -rf .git");
// $sReturn.=$this->_execAndSend("cd $sTempDir && rm -rf .svn");
// $sReturn.=$this->_execAndSend("find $sTempDir -type d -name '.svn' -exec rm -rf {} \;");
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// create package
// --------------------------------------------------
$sReturn .= '<h3>' . t('class-project-build-label-create-package') . '</h3>';
// public_html must exist
if (isset($this->_aPrjConfig["build"]['haspublic'][0])) {
$sWebroot = false;
$sWebroot1 = "$sTempBuildDir/public_html";
$sWebroot2 = "$sTempBuildDir/public";
if (file_exists($sWebroot1)) {
$sWebroot = $sWebroot1;
}
if (file_exists($sWebroot2)) {
$sWebroot = $sWebroot2;
}
if (!$sWebroot) {
$sError = t('class-project-error-build-docroot-not-found');
$this->_logaction($sError, __FUNCTION__, "error");
$this->_TempFill($sError . $sReturn, $aActionList);
$this->_TempDelete($sTempBuildDir);
return $this->_oHtml->getBox("error", $sError . $sReturn . $sError);
}
}
if (!$this->_iRcAll == 0) {
$sError = sprintf(t('class-project-error-command-failed'), $sTempBuildDir);
$this->_logaction($sError, __FUNCTION__, "error");
$this->_TempFill($sError . $sReturn, $aActionList);
$this->_TempDelete($sTempBuildDir);
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
// $sReturn.=$this->_oHtml->getBox("success", "preparations ok - directory is ready for packaging now.");
// --- generate info file
$sTs = date("Y-m-d H:i:s");
$sTs2 = date("Ymd_His");
// $sBranch = ($this->_sBranchname ? $this->_sBranchname : t("defaultbranch"));
$sInfoFileWebroot = $sTempBuildDir . '/' . basename($this->_getInfofile($sFirstLevel, "deployed"));
$sInfoFileArchiv = $this->_getArchiveDir($sTs2) . '/' . basename($this->_getInfofile($sFirstLevel, "deployed"));
$sPackageFileArchiv = $this->_getArchiveDir($sTs2) . '/' . basename($this->_getPackagefile($sFirstLevel, "deployed"));
$aInfos = [
'date' => $sTs,
'version' => $sTs2,
// 'branch' => $sBranch,
'branch' => $aRepodata['branch'],
'branch_short' => $aRepodata['shortname'],
'branch_type' => $aRepodata['type'],
'revision' => $aRepodata['revision'],
'revision_short' => $sRevisionShort,
'imagepart' => $aCivars['imagepart'],
'message' => $aRepodata['message'],
];
/*
"user": "' . $aParams["inputUser"] . '",
"remark": "' . $aParams["inputComment"] . '"
*/
$sReturn .= t("class-project-info-build-write-meta-to-webroot") . "<br>";
// file_put_contents($sInfoFileWebroot, $sInfos);
file_put_contents($sInfoFileWebroot, json_encode($aInfos));
$sReturn .= $this->_execAndSend("ls -l $sInfoFileWebroot");
$sReturn .= $this->_execAndSend("cat $sInfoFileWebroot");
if (!file_exists(dirname($sPackageFileArchiv))) {
$sReturn .= sprintf(t("creating-directory"), dirname($sPackageFileArchiv)) . "<br>";
mkdir(dirname($sPackageFileArchiv), 0775, true);
}
$sReturn .= $this->_execAndSend("ls -ld " . dirname($sPackageFileArchiv));
if (!file_exists(dirname($sPackageFileArchiv))) {
$sError = sprintf(t('"class-project-error-build-dir-was-not-created"'), $sTempBuildDir);
$this->_logaction($sError, __FUNCTION__, "error");
$this->_TempFill($sError . $sReturn, $aActionList);
$this->_TempDelete($sTempBuildDir);
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
$this->_TempFill($sReturn, $aActionList);
// ----- loop over enabled build plugins
// WIP
// set name of the activated plugin for this project
$aPlugins = (isset($this->_aPrjConfig['build']['enabled_build_plugins']) && $this->_aPrjConfig['build']['enabled_build_plugins'])
? $this->_aPrjConfig['build']['enabled_build_plugins']
: ['tgz'];
foreach ($aPlugins as $sPluginName) {
$oPlugin = false;
$sReturn .= '<h4>' . $sPluginName . '</h4>';
try {
include_once $this->_getPluginFilename('build', $sPluginName);
$sPluginClassname = 'build_' . $sPluginName;
$oPlugin = new $sPluginClassname([
'lang' => $this->_aConfig['lang'],
'workdir' => $sTempBuildDir,
'outfile' => $sPackageFileArchiv,
]);
} catch (Exception $ex) {
return $this->_oHtml->getBox(
"error",
"FAILED to initialize build plugin " . $sPluginName . '<br>'
. $sReturn
);
}
$sReturn .= sprintf(t("creating-file"), $oPlugin->getOutfile()) . "<br>";
foreach ($oPlugin->checkRequirements() as $sCommand) {
$sReturn .= $this->_execAndSend($sCommand);
$this->_TempFill($sReturn, $aActionList);
}
foreach ($oPlugin->getBuildCommands() as $sCommand) {
$sReturn .= $this->_execAndSend($sCommand);
$this->_TempFill($sReturn, $aActionList);
}
}
// write info file (.json)
$sReturn .= sprintf(t("creating-file"), $sInfoFileArchiv) . "<br>";
// file_put_contents($sInfoFileArchiv, $sInfos);
file_put_contents($sInfoFileArchiv, json_encode($aInfos));
// copy template files
if (file_exists($sTempBuildDir . '/hooks/templates/')) {
$sReturn .= t("class-project-info-build-write-templatefiles-to-archive") . "<br>";
$sReturn .= $this->_execAndSend("cp $sTempBuildDir/hooks/templates/* " . $this->_getArchiveDir($sTs2));
} else {
$sReturn .= t("class-project-info-build-write-templatefiles-to-archive-skipped") . "<br>";
}
$this->_TempFill($sReturn, $aActionList);
$sReturn .= "<br>" . t("info") . ":<br>";
$sReturn .= $this->_execAndSend("ls -l " . $this->_getArchiveDir($sTs2));
// TEST
// $this->_iRcAll=1;
if (!$this->_iRcAll == 0) {
$sError = t('class-project-error-build-packaging-failed');
$this->_logaction($sError, __FUNCTION__, "error");
$this->_TempFill($sError . $sReturn, $aActionList);
$this->_TempDelete($sTempBuildDir);
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
$sReturn .= '<h3>' . t("class-project-build-label-remove-workdir") . '</h3>';
$sReturn .= $this->_execAndSend("rm -rf $sTempBuildDir");
$sReturn .= t("class-project-info-build-remove-oldest-archives");
$sReturn .= '<pre>' . print_r($this->cleanupArchive(), true) . '</pre>';
$sInfo = t("class-project-info-build-successful");
$this->_logaction(t('finished') . ' ' . $sInfo, __FUNCTION__, "success");
$sReturn .= $this->_oHtml->getBox("success", $sInfo);
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
$sReturn .= $this->queue($sFirstLevel, $sTs2);
$this->_TempDelete();
$this->_setProcessOutFile(false);
return $sReturn;
}
/**
* Put a packaged version into the queue of a specified phase
*
* @param string $sPhase name of the phase that gets the new version
* @param string $sVersion version
* @return string The HTML code
*/
public function queue(string $sPhase, string $sVersion): string
{
$aActionList = [
'iActive' => 0,
'label' => t("queue"),
'actions' => [
['label' => t("class-project-queue-label-checks")],
['label' => t("class-project-queue-label-remove-existing-version")],
['label' => t("class-project-queue-label-link-new-version")],
['label' => t("class-project-queue-label-deploy")],
],
];
$this->_logaction(t('starting') . " queue($sPhase, $sVersion)", __FUNCTION__);
$sReturn = "<h2> " . t("queue") . " " . $this->getLabel() . " :: $sPhase</h2>";
$this->_TempFill($sReturn, $aActionList);
if (!$this->isActivePhase($sPhase)) {
$sError = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
$this->_logaction($sError, __FUNCTION__, "error");
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
$sPlace = "onhold";
$sLinkTarget = $this->_getArchiveDir($sVersion);
$sLinkName = $this->_getFileBase($sPhase, $sPlace);
// --------------------------------------------------
// Checks
// --------------------------------------------------
if (!$sLinkName) {
die(t("class-project-error-queue-sLinkName-is-empty"));
}
if (!$sLinkTarget) {
die(t("class-project-error-queue-sLinkTarget-is-empty"));
}
if (!file_exists($sLinkTarget)) {
die(sprintf(t("class-project-error-queue-wrong-version"), $sVersion, $sLinkTarget));
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// remove existing version
// --------------------------------------------------
$this->_iRcAll = 0;
if (file_exists($sLinkName)) {
$sReturn .= t("class-project-queue-label-remove-existing-version") . "<br>";
$sReturn .= $this->_execAndSend("rm -f $sLinkName");
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// create the new link
// --------------------------------------------------
$sReturn .= t("class-project-queue-label-link-new-version") . "<br>";
$sReturn .= $this->_execAndSend("ln -s $sLinkTarget $sLinkName");
$sReturn .= $this->_execAndSend("ls -l $sLinkName | fgrep $sLinkTarget");
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
if (!$this->_iRcAll == 0) {
$this->_TempDelete();
$sError = t("class-project-error-command-failed");
$this->_logaction($sError, __FUNCTION__, "error");
return $this->_oHtml->getBox("error", $sError . $sReturn);
}
$this->_logaction(t('finished') . " queue($sPhase, $sVersion) " . t("class-project-info-queue-successful"), __FUNCTION__);
$sReturn .= $this->_oHtml->getBox("success", t("class-project-info-queue-successful"));
$sReturn .= $this->deploy($sPhase);
$this->_TempDelete();
return $sReturn;
}
/**
* Deploy a queued package - this moves the queue into the repo directory.
* This method checks the deploy times
* It returns the output to show in browser
*
* @param string $sPhase the queue of which phase we want to install in server
* @param bool $bIgnoreDeploytimes flag; if true it will override time windows
* @return string The HTML output
*/
public function deploy(string $sPhase, bool $bIgnoreDeploytimes = false): string
{
$this->log(__FUNCTION__ . " start");
if (
!$this->oUser->hasPermission("project-action-deploy") && !$this->oUser->hasPermission("project-action-deploy-$sPhase")
) {
return $this->oUser->showDenied();
}
$aActionList = [
'iActive' => 0,
'label' => t("deploy"),
'actions' => [
['label' => t("class-project-deploy-label-checks")],
['label' => t("class-project-deploy-label-activate-queued-version")],
['label' => t("class-project-deploy-label-synch-packages")],
['label' => t("class-project-deploy-label-install-on-target")],
],
];
$sReturn = "<h2>" . t("deploy") . " " . $this->getLabel() . " :: $sPhase</h2>";
$this->_TempFill($sReturn, $aActionList);
if (!$this->isActivePhase($sPhase)) {
$sError = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
$this->_logaction($sError, __FUNCTION__, "error");
return $sReturn . $this->_oHtml->getBox("error", $sError);
}
$sQueueLink = $this->_getFileBase($sPhase, "onhold");
$sRepoLink = $this->_getFileBase($sPhase, "ready2install");
// --------------------------------------------------
// checks
// --------------------------------------------------
$sReturn .= "<h3>" . t("class-project-deploy-label-checks") . "</h3>";
$aDeploytimes = $this->_getDeploytimes($sPhase);
if (count($aDeploytimes)) {
// check if the a deploy time is reached
$sNow = date("D H:i:s");
$sReturn .= sprintf(t("class-project-info-deploy-check-deployment-times"), $sNow) . "<br>";
$bCanDeploy = false;
foreach ($aDeploytimes as $sRegex) {
$sReturn .= sprintf(t("class-project-info-deploy-test-regex"), $sRegex);
if (preg_match($sRegex, $sNow)) {
$sReturn .= t("ok");
$bCanDeploy = true;
} else {
$sReturn .= t("no");
}
$sReturn .= "<br>";
}
if (!$bCanDeploy) {
if (!$bIgnoreDeploytimes) {
$sError = t("class-project-info-deploy-time-not-reached");
// $this->_logaction($sError, __FUNCTION__);
$sReturn .= $this->_oHtml->getBox("info", $sError);
$this->_TempDelete();
// removed: cronjob sends this message too
// $this->_sendMessage($sError."\n".t('phase').': '.$sPhase);
return $sReturn;
} else {
$sReturn .= t("class-project-info-deploy-time-not-reached-and-ignored") . "<br>";
}
} else {
$sReturn .= t("class-project-info-deploy-time-ok") . "<br>";
}
// if ()
}
$this->_logaction(t('starting') . " deploy($sPhase, $bIgnoreDeploytimes)", __FUNCTION__);
if (!file_exists($sQueueLink)) {
$sError = sprintf(t("class-project-info-deploy-nothing-in-queue"), $sQueueLink);
$this->_logaction($sError, __FUNCTION__, "error");
$sReturn .= $this->_oHtml->getBox("info", $sError);
$this->_TempDelete();
$this->_sendMessage($sError . "\n" . t('phase') . ': ' . $sPhase);
return $sReturn;
}
$this->_TempFill($sReturn);
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// move the queue link to the repo name
// --------------------------------------------------
$this->_iRcAll = 0;
if (file_exists($sRepoLink)) {
$sReturn .= t("class-project-info-deploy-removing-existing-version") . "<br>";
$sReturn .= $this->_execAndSend("rm -f $sRepoLink");
}
$this->_TempFill($sReturn);
$sReturn .= t("class-project-info-deploy-moving-queue-to-repo") . "<br>";
$sReturn .= $this->_execAndSend("mv $sQueueLink $sRepoLink");
if (!$this->_iRcAll == 0) {
$this->_TempDelete();
$sError = t("class-project-error-command-failed");
$this->_logaction($sError, __FUNCTION__, "error");
$sReturn .= $this->_oHtml->getBox("error", $sError . $sReturn);
$this->_sendMessage($sError . "\n" . t('phase') . ': ' . $sPhase);
return $sReturn;
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// synch packages
// --------------------------------------------------
// $sReturn.=$this->_execAndSend("ln -s $sLinkTarget $sLinkName");
if (isset($this->_aConfig['mirrorPackages']) && count($this->_aConfig['mirrorPackages'])) {
foreach ($this->_aConfig['mirrorPackages'] as $sLabel => $aTarget) {
$sReturn .= '<h3>' . sprintf(t("class-project-info-deploy-synching-package"), $sLabel) . "</h3>";
if (isset($aTarget['type'])) {
$sCmd = false;
// $sSource=$this->_aConfig["packageDir"]."/$sPhase/*";
$sSource = $sRepoLink;
$sTarget = $aTarget['target'] . "/$sPhase";
switch ($aTarget['type']) {
case 'rsync':
$sCmd = "ls -l $sSource 2>/dev/null && /usr/bin/rsync --delete -rLvt $sSource $sTarget";
break;
default:
$sReturn .= sprintf(t("class-project-info-deploy-skip-sync"), $aTarget['type']) . "<br>";
break;
} // switch
if ($sCmd) {
/*
if ($aTarget['runas']) {
$sCmd="su - " . $aTarget['runas'] . " -c \"" . $sCmd . "\"";
}
*
*/
$sReturn .= $this->_execAndSend($sCmd);
$this->_TempFill($sReturn);
}
}
} // foreach
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
// --------------------------------------------------
// run action to install
// --------------------------------------------------
$sDeploymethod = isset($this->_aPrjConfig["phases"][$sPhase]["deploymethod"]) ? $this->_aPrjConfig["phases"][$sPhase]["deploymethod"] : "none";
// $sTargethosts = array_key_exists("hosts", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["hosts"] : '';
$sReturn .= '<h3>' . t("class-project-info-deploy-start-by-method") . ' :: ' . $sDeploymethod . '</h3>'
. '<p>'
. t("deploymethod-$sDeploymethod") . '<br>'
// . t("phase-targethosts") . ': ' . ($sTargethosts ? $sTargethosts : t("none"))
. '</p>';
if ($sDeploymethod === "none") {
$sReturn .= t("class-project-info-deploy-start-by-method-skip") . "<br>";
} else {
$sReturn .= '<p>Plugin: ' . $this->oRolloutPlugin->getId() . '</p>';
foreach ($this->oRolloutPlugin->getDeployCommands($sPhase) as $sCmd) {
$sReturn .= $this->_execAndSend("$sCmd");
}
/*
$aTargethosts = explode(',', $sTargethosts);
foreach ($aTargethosts as $sTargethost) {
$sReturn.='<h4>' . $sDeploymethod . ' - ' . $sTargethost . '</h4>';
$sCmd = '';
switch ($sDeploymethod) {
case 'puppet':
$sCmd = 'ssh ' . $this->_aConfig["installPackages"]["user"]
. '@' . $sTargethost
. ' ' . $this->_aConfig["installPackages"]["command"];
break;
;
// TODO: we don't have any proxy yet
case 'sshproxy__AS_EXAMPLE_ONLY':
$sCmd = 'ssh ' . $this->_aConfig["installPackages"]["sshproxy"]["user"]
. '@' . $this->_aConfig["installPackages"]["sshproxy"]["host"]
. ' ' . sprintf($this->_aConfig["installPackages"]["sshproxy"]["command"], $sTargethost);
break;
;
}
if ($sCmd) {
// $sReturn.=$sCmd.'<br>';
$sReturn.=$this->_execAndSend("$sCmd");
}
}
*
*/
}
$aActionList['iActive']++;
$this->_TempFill($sReturn, $aActionList);
$sReturn .= "<br>";
if (!$this->_iRcAll == 0) {
$sWarnlevel = 'error';
$sMessage = sprintf(t('class-project-info-deploy-failed'), $sPhase);
} else {
$sWarnlevel = 'success';
$sMessage = sprintf(t("class-project-info-deploy-successful"), $sPhase);
}
$sReturn .= $this->_oHtml->getBox($sWarnlevel, $sMessage);
$this->_sendMessage($sMessage);
$this->_logaction(t('finished') . " deploy($sPhase, $bIgnoreDeploytimes) " . $sMessage, __FUNCTION__, $sWarnlevel);
$this->_TempDelete();
return $sReturn;
}
/**
* Accept a the installed version of the given phase and put this version
* to the queue of the next phase.
*
* @param string $sPhase phase to accept to be rolled out on the next phase
* @return string The HTML code
*/
public function accept(string $sPhase): string
{
$this->log(__FUNCTION__ . " start");
if (
!$this->oUser->hasPermission("project-action-accept") && !$this->oUser->hasPermission("project-action-accept-$sPhase")
) {
return $this->oUser->showDenied();
}
$sReturn = "<h2>" . t("accept") . " " . $this->getLabel() . " :: $sPhase</h2>";
$this->_logaction(t('starting') . " accept($sPhase)", __FUNCTION__);
if (!$this->canAcceptPhase($sPhase)) {
$sError = sprintf(t("class-project-error-accept-impossible"), $sPhase);
$this->_logaction($sError, __FUNCTION__, "error");
return $sReturn . $this->_oHtml->getBox("error", $sError);
}
$sReturn .= "<h3>" . sprintf(t("class-project-info-accept-overview"), $sPhase) . "</h3>";
$this->_TempFill($sReturn);
$aInfos = $this->getPhaseInfos($sPhase);
$sVersion = $aInfos["deployed"]["version"];
$sNext = $this->getNextPhase($sPhase);
// $sReturn.='<pre>' . print_r($aInfos["deployed"], true) . '</pre>';
$sReturn .= $this->_oHtml->getBox("info", sprintf(t("class-project-info-accept-version-and-next-phase"), $sVersion, $sNext));
$this->_logaction(t('finished') . " accept($sPhase) " . sprintf(t("class-project-info-accept-version-and-next-phase"), $sVersion, $sNext), __FUNCTION__, "success");
$sReturn .= $this->queue($sNext, $sVersion);
$this->_TempFill($sReturn);
$this->_TempDelete();
return $sReturn;
}
/**
* Save data as project config.
* If no data were given then $_POST is used.
* It returns bool with success state or a string with deny error message
*
* @param array $aData optional: data to write
* @return boolean|string
*/
public function saveConfig(array $aData = []): bool|string
{
$this->log(__FUNCTION__ . " start");
if (!$this->oUser->hasPermission("project-action-setup")) {
return $this->oUser->showDenied();
}
$this->_logaction(t('starting') . " saveConfig(...)", __FUNCTION__);
if (!count($aData)) {
$aData = $_POST;
}
foreach (['id', 'label', 'description', 'contact', 'build', 'fileprefix', 'phases'] as $sKey) {
if (!isset($aData[$sKey])) {
$this->_logaction(t('abortet') . " missing key $sKey in savedata", __FUNCTION__, "error");
return false;
}
}
$sId = $aData["id"];
// remove unwanted items
foreach (["setupaction", "prj", "id"] as $s) {
if (isset($aData[$s])) {
unset($aData[$s]);
}
}
// save json file
if ($this->_aConfig['projects']['json']['active']) {
// echo "IST <pre>" . print_r($this->_aPrjConfig, true) . "</pre>"; echo "NEU <pre>" . print_r($aData, true) . "</pre>"; die();
// make a backup of a working config
$sCfgFile = $this->_getConfigFile($sId);
$sBakFile = $this->_getConfigFile($sId) . ".ok";
copy($sCfgFile, $sBakFile);
$bReturn = file_put_contents($sCfgFile, json_encode($aData, JSON_PRETTY_PRINT));
$this->_aPrjConfig = json_decode(file_get_contents($this->_getConfigFile($sId)), true);
}
// save in ldap
if ($this->_aConfig['projects']['ldap']['active']) {
// TODO:
echo "TODO: save in LDAP<br><pre>" . print_r($aData, 1) . "</pre>";
$sDn = 'documentIdentifier=' . $sId . ',' . $this->_aConfig['projects']['ldap']['DnProjects'];
$aItem = [
'objectClass' => [
'document',
'hieraSource',
'top',
],
'hieraData' => [
'cfg=' . json_encode($aData),
'updated=' . date("Y-m-d H:i:s") . ' by ' . $this->oUser->getUsername(),
]
];
require_once("ldap.class.php");
$oLdapIML = new imlldap($this->_aConfig['projects']['ldap']);
//$oLdapIML->debugOn();
if (!$oLdapIML->DnExists($sDn)) {
if ($oLdapIML->objAdd($sDn, $aItem)) {
echo 'OK, created in LDAP.<br>';
$bReturn = true;
} else {
echo 'ERROR, DN ' . $sDn . ' was not created in LDAP :-/<br>';
$bReturn = false;
}
} else {
if ($oLdapIML->objUpdate($sDn, $aItem)) {
echo 'OK, updated in LDAP.<br>';
$bReturn = true;
} else {
echo 'ERROR, DN ' . $sDn . ' was not updated in LDAP :-/<br>';
$bReturn = false;
}
}
$oLdapIML->close();
}
$this->_logaction(t('finished') . " saveConfig(...)", __FUNCTION__, "success");
$this->setProjectById($sId);
$sMessage = ($bReturn
? t("page-setup-info-settings-were-saved")
: t("page-setup-error-settings-were-not-saved")
);
$this->_sendMessage($sMessage);
return $bReturn;
}
/**
* Create a new project; it returns the error message if it fails and
* an empty string if it was successful.
*
* @param string $sId id
* @return string
*/
public function create(string $sId): string
{
$this->log(__FUNCTION__ . " start");
if (!$this->oUser->hasPermission("project-action-create")) {
return $this->oUser->showDenied();
}
$this->_logaction(t('starting') . " create($sId)", __FUNCTION__);
if (!$sId) {
$sError = t("class-project-error-create-missing-id");
$this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
return $sError;
}
$s = preg_replace('/[a-z\-\_0-9]*/', "", $sId);
if ($s) {
$sError = sprintf(t("class-project-error-create-wrcng-chars-in-id"), $sId);
$this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
return $sError;
}
if ($sId == "all") {
$sError = sprintf(t("class-project-error-create-id-has-reserved-name"), $sId);
$this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
return $sError;
}
if (array_search($sId, $this->getProjects()) !== false) {
$sError = sprintf(t("class-project-error-create-id-exists"), $sId);
$this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
return $sError;
}
// reset config and create a skeleton
$this->_readConfig();
$this->_aConfig["id"] = $sId;
$this->_aPrjConfig = [
"id" => $sId, // for saveConfig
"label" => "$sId",
"fileprefix" => "$sId",
"description" => '',
"contact" => '',
"build" => [
"type" => "",
"ssh" => "",
"auth" => "",
"webaccess" => "",
],
"phases" => [
"preview" => [],
"stage" => [],
"live" => [],
],
];
$this->_verifyConfig(); // check skeleton
$bReturn = $this->saveConfig($this->_aPrjConfig);
if (!$bReturn) {
$sError = t("class-project-error-create-save-failed");
$this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
return $sError;
}
// alles OK - dann leeren String
$this->_logaction(t('finished') . " create($sId)", __FUNCTION__, "success");
return "";
}
/**
* Delete a project; it returns a string with errormessage; empty string = no error
*
* @param array $aOptions array with enabled actions
* - bRemoveRepolinks
* - bRemoveArchive
* - bRemoveConfig
* @return string
*/
public function delete(array $aOptions = []): string
{
$this->log(__FUNCTION__ . " start");
if (!$this->oUser->hasPermission("project-action-delete")) {
return $this->oUser->showDenied();
}
$sCfgfile = $this->_getConfigFile($this->_aConfig["id"]);
if (!file_exists($sCfgfile)) {
return t("class-project-error-delete-project-no-configfile");
}
$this->_logaction(t('starting') . " delete()", __FUNCTION__);
// ["bRemoveRepolinks", "bRemoveArchive", "bRemoveConfig"]
// --- remove links in phases directory to built archives
if (isset($aOptions["bRemoveRepolinks"]) && $aOptions["bRemoveRepolinks"]) {
echo "DELETE Repo-Links ...<br>";
foreach (array_keys($this->getPhases()) as $sPhase) {
foreach (array_keys($this->_aPlaces) as $sPlace) {
$sLink = $this->_getFileBase($sPhase, $sPlace);
if (file_exists($sLink)) {
echo "Removing $sLink ($sPhase - $sPlace)...<br>";
if (!unlink($sLink)) {
$sError = t("class-project-error-delete-project-deletion-failed-data");
$this->_logaction(t('aborted') . " " . $sError, __FUNCTION__);
return $sError;
}
}
}
}
}
if (isset($aOptions["bRemoveArchive"]) && $aOptions["bRemoveArchive"]) {
echo "DELETE built Archives ...<br>";
$this->cleanupArchive(true); // true to delete all
}
if (isset($aOptions["bRemoveConfig"]) && $aOptions["bRemoveConfig"]) {
echo "DELETE Config ...<br>";
// echo "config file: $sCfgfile<br>";
if (file_exists($sCfgfile . ".ok")) {
// echo "Delete $sCfgfile.ok<br>";
unlink($sCfgfile . ".ok");
}
if (file_exists($sCfgfile)) {
// echo "Delete $sCfgfile<br>";
if (!unlink($sCfgfile)) {
$sError = t("class-project-error-delete-project-deletion-failed-configfile");
$this->_logaction(t('aborted') . " " . $sError, __FUNCTION__);
return $sError;
}
}
}
$this->_sendMessage(t('finished') . " delete()");
$this->_logaction(t('finished') . " delete()", __FUNCTION__, "success");
return '';
}
}