diff --git a/config/config.php b/config/config.php index de72f402df..a5804d5c6c 100644 --- a/config/config.php +++ b/config/config.php @@ -203,3 +203,24 @@ else unset($config->bizVersion); unset($config->maxVersion); } + +/* 渠成平台设置。CNE Api settings. */ +$config->CNE = new stdclass(); +$config->CNE->api = new stdclass(); +$config->CNE->api->host = ''; +$config->CNE->api->auth = 'X-Auth-Token'; +$config->CNE->api->token = 'reiquai7pei7thei4iePe5ait3Kaiquu'; // Please set token in my.php. +$config->CNE->api->headers = array('Content-Type: application/json'); +$config->CNE->api->channel = 'stable'; + +$config->CNE->app = new stdclass; +$config->CNE->app->domain = 'dev.haogs.cn'; + +$config->cloud = new stdclass; +$config->cloud->api = new stdclass; +$config->cloud->api->host = 'http://cne-market.internal-pre.chandao.net'; +$config->cloud->api->auth = 'X-Auth-Token'; +$config->cloud->api->token = 'gwaN4KynqNqQoPD7eN8s'; // Please set token in my.php. +$config->cloud->api->headers = array('Content-Type: application/json'); +$config->cloud->api->channel = 'stable'; +$config->cloud->api->switchChannel = false; diff --git a/config/zentaopms.php b/config/zentaopms.php index d4fce51996..f01d660f22 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -383,6 +383,10 @@ if(!defined('TABLE_MEASRECORDS')) define('TABLE_MEASRECORDS', '`' . $conf if(!defined('TABLE_MEASQUEUE')) define('TABLE_MEASQUEUE', '`' . $config->db->prefix . 'measqueue`'); if(!defined('TABLE_MEASTEMPLATE')) define('TABLE_MEASTEMPLATE', '`' . $config->db->prefix . 'meastemplate`'); +define('TABLE_SPACE', '`' . $config->db->prefix . 'space`'); +define('TABLE_INSTANCE', '`' . $config->db->prefix . 'instance`'); +define('TABLE_SOLUTION', '`' . $config->db->prefix . 'solution`'); + $config->objectTables['product'] = TABLE_PRODUCT; $config->objectTables['productplan'] = TABLE_PRODUCTPLAN; $config->objectTables['story'] = TABLE_STORY; diff --git a/db/update20.0.sql b/db/update20.0.sql index 825b3faa84..233067bd7b 100644 --- a/db/update20.0.sql +++ b/db/update20.0.sql @@ -28,3 +28,76 @@ ALTER TABLE `zt_project` DROP `division`; ALTER TABLE `zt_bug` CHANGE `linkBug` `relatedBug` varchar(255) NOT NULL DEFAULT ''; ALTER TABLE `zt_product` ADD COLUMN `groups` text NULL AFTER `acl`; + + +-- DROP TABLE IF EXISTS `zt_space`; +CREATE TABLE `zt_space` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(200) NOT NULL, + `k8space` char(64) NOT NULL, + `owner` char(30) NOT NULL, + `default` tinyint(1) NOT NULL DEFAULT 0, + `createdAt` datetime NOT NULL, + `deleted` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `name` (`name`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- DROP TABLE IF EXISTS `zt_instance`; +CREATE TABLE IF NOT EXISTS `zt_instance` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `space` mediumint(8) unsigned NOT NULL, + `solution` mediumint(8) unsigned NOT NULL, + `name` char(50), + `appID` mediumint(8) unsigned NOT NULL, + `appName` char(50) NOT NULL, + `appVersion` char(20) NOT NULL, + `chart` char(50) NOT NULL, + `logo` varchar(255), + `version` char(50) NOT NULL, + `desc` text, + `introduction` varchar(500), + `source` char(20) NOT NULL, + `channel` char(20), + `k8name` char(64) NOT NULL, + `status` char(20) NOT NULL, + `pinned` enum('0', '1') NOT NULL DEFAULT '0', + `domain` char(255) NOT NULL, + `smtpSnippetName` char(30) NULL, + `ldapSnippetName` char(30) NULL, + `ldapSettings` text, + `dbSettings` text, + `autoBackup` tinyint(1) NOT NULL DEFAULT 0, + `backupKeepDays` int unsigned NOT NULL DEFAULT 1, + `autoRestore` tinyint(1) NOT NULL DEFAULT 0, + `env` text, + `createdBy` char(30) NOT NULL, + `createdAt` datetime NOT NULL, + `deleted` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `space` (`space`), + KEY `k8name` (`k8name`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- DROP TABLE IF EXISTS `zt_solution`; +CREATE TABLE IF NOT EXISTS `zt_solution` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `name` char(50), + `appID` mediumint(8) unsigned NOT NULL, + `appName` char(50) NOT NULL, + `appVersion` char(20) NOT NULL, + `version` char(50) NOT NULL, + `chart` char(50) NOT NULL, + `cover` varchar(255), + `desc` text, + `introduction` varchar(500), + `source` char(20) NOT NULL, + `channel` char(20), + `components` text, + `status` char(20) NOT NULL, + `deleted` tinyint(1) NOT NULL DEFAULT 0, + `createdBy` char(30) NOT NULL, + `createdAt` datetime NOT NULL, + `updatedDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; diff --git a/module/common/lang/common.php b/module/common/lang/common.php index 041c575979..bcaa9692dd 100644 --- a/module/common/lang/common.php +++ b/module/common/lang/common.php @@ -95,6 +95,8 @@ $lang->zanode = new stdclass(); $lang->holiday = new stdclass(); $lang->contact = new stdclass(); $lang->dimension = new stdclass(); +$lang->space = new stdclass(); +$lang->store = new stdclass(); $lang->programstakeholder = new stdclass(); $lang->researchplan = new stdclass(); diff --git a/module/common/lang/en.php b/module/common/lang/en.php index 4a2e7d9206..c003c6a29f 100644 --- a/module/common/lang/en.php +++ b/module/common/lang/en.php @@ -229,6 +229,7 @@ $lang->zahost->common = 'ZAhost'; $lang->zanode->common = 'ZAnode'; $lang->dimension->common = 'Dimension'; $lang->contact->common = 'Contacts'; +$lang->space->common = 'Services'; $lang->programstakeholder->common = 'Stakeholder'; $lang->featureswitch->common = 'Features On/Off'; @@ -362,6 +363,7 @@ $lang->devops->mr = 'Merge Request'; $lang->devops->repo = 'Repo'; $lang->devops->rules = 'Rule'; $lang->devops->settings = 'Setting Merge Request'; +$lang->devops->platform = 'Platform'; $lang->devops->set = 'Set'; $lang->admin->module = 'Module'; @@ -478,6 +480,7 @@ $lang->error->editedByOther = 'This record might have been changed. Please ref $lang->error->tutorialData = 'No data can be imported in tutorial mode. Please quit tutorial first!'; $lang->error->noCurlExt = 'No Curl module installed'; $lang->error->loginTimeout = 'Login has timed out, please login again!'; +$lang->error->httpServerError = 'Server error'; /* Page info. */ $lang->pager = new stdclass(); diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php index d59c94b31e..d02f408e1d 100644 --- a/module/common/lang/menu.php +++ b/module/common/lang/menu.php @@ -471,19 +471,21 @@ $lang->qa->dividerMenu = ',bug,testtask,caselib,automation,'; /* DevOps menu. */ $lang->devops->menu = new stdclass(); -$lang->devops->menu->code = array('link' => "{$lang->repo->common}|repo|browse|repoID=%s", 'alias' => 'diff,view,revision,log,blame,showsynccommit'); -$lang->devops->menu->mr = array('link' => "{$lang->devops->mr}|mr|browse|repoID=%s"); -$lang->devops->menu->compile = array('link' => "{$lang->devops->compile}|job|browse|repoID=%s", 'subModule' => 'compile,job'); -$lang->devops->menu->app = array('link' => "{$lang->app->common}|app|serverlink|%s"); -$lang->devops->menu->set = array('link' => "{$lang->devops->set}|repo|maintain", 'subModule' => 'gitlab,jenkins,sonarqube,gitea,gogs', 'alias' => 'setrules,create,edit'); +$lang->devops->menu->code = array('link' => "{$lang->repo->common}|repo|browse|repoID=%s", 'alias' => 'diff,view,revision,log,blame,showsynccommit'); +$lang->devops->menu->mr = array('link' => "{$lang->devops->mr}|mr|browse|repoID=%s"); +$lang->devops->menu->compile = array('link' => "{$lang->devops->compile}|job|browse|repoID=%s", 'subModule' => 'compile,job'); +$lang->devops->menu->app = array('link' => "{$lang->app->common}|app|serverlink|%s"); +$lang->devops->menu->platform = array('link' => "{$lang->devops->platform}|space|browse", 'subModule' => 'instance,store'); +$lang->devops->menu->set = array('link' => "{$lang->devops->set}|repo|maintain", 'subModule' => 'gitlab,jenkins,sonarqube,gitea,gogs', 'alias' => 'setrules,create,edit'); $lang->devops->menuOrder[5] = 'code'; $lang->devops->menuOrder[10] = 'mr'; $lang->devops->menuOrder[15] = 'compile'; $lang->devops->menuOrder[20] = 'app'; -$lang->devops->menuOrder[25] = 'set'; +$lang->devops->menuOrder[25] = 'platform'; +$lang->devops->menuOrder[30] = 'set'; -$lang->devops->dividerMenu = ',set,'; +$lang->devops->dividerMenu = ',platform,'; $lang->devops->menu->set['subMenu'] = new stdclass(); $lang->devops->menu->set['subMenu']->repo = array('link' => "{$lang->devops->repo}|repo|maintain", 'alias' => 'create,edit'); @@ -667,6 +669,8 @@ $lang->navGroup->git = 'devops'; $lang->navGroup->app = 'devops'; $lang->navGroup->pipeline = 'devops'; $lang->navGroup->devopssetting = 'devops'; +$lang->navGroup->space = 'devops'; +$lang->navGroup->instance = 'devops'; $lang->navGroup->company = 'system'; $lang->navGroup->systemteam = 'system'; diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php index 3299681d48..29c62c04ad 100644 --- a/module/common/lang/zh-cn.php +++ b/module/common/lang/zh-cn.php @@ -229,6 +229,8 @@ $lang->zahost->common = '宿主机'; $lang->zanode->common = '执行节点'; $lang->dimension->common = '维度'; $lang->contact->common = '联系人'; +$lang->space->common = '服务管理'; +$lang->store->common = '应用市场'; $lang->programstakeholder->common = '干系人'; $lang->featureswitch->common = '功能开关'; @@ -362,6 +364,7 @@ $lang->devops->mr = '合并请求'; $lang->devops->repo = '代码库'; $lang->devops->rules = '指令'; $lang->devops->settings = '合并请求设置'; +$lang->devops->platform = '平台'; $lang->devops->set = '设置'; $lang->admin->module = '功能配置'; @@ -478,6 +481,7 @@ $lang->error->editedByOther = '该记录可能已经被改动。请刷新页 $lang->error->tutorialData = '新手模式下不会插入数据,请退出新手模式操作'; $lang->error->noCurlExt = '服务器未安装Curl模块。'; $lang->error->loginTimeout = '登录已超时,请重新登入!'; +$lang->error->httpServerError = '服务器错误'; /* 分页信息。*/ $lang->pager = new stdclass(); diff --git a/module/common/model.php b/module/common/model.php index a9a8c1a24e..016151f237 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -3683,6 +3683,96 @@ EOF; return false; } + + + /** + * Get method of API. + * + * @param string $url + * @param array|object $data + * @param array $headers example: array("key1:value1", "key2:value2") + * @access public + * @return object + */ + public static function apiGet($url, $data = array(), $headers = array()) + { + $url .= (strpos($url, '?') !== false ? '&' : '?') . http_build_query($data, '', '&', PHP_QUERY_RFC3986); + + $result = json_decode(commonModel::http($url, $data, array(CURLOPT_CUSTOMREQUEST => 'GET'), $headers, 'json', 20)); + if($result && $result->code == 200) return $result; + + return self::apiError($result); + } + + /** + * Post method of API. + * + * @param string $url + * @param array|object $data + * @param array $headers example: array("key1:value1", "key2:value2") + * @access public + * @return object + */ + public static function apiPost($url, $data, $headers = array()) + { + $result = json_decode(commonModel::http($url, $data, array(CURLOPT_CUSTOMREQUEST => 'POST'), $headers, 'json', 20)); + if($result && $result->code == 200) return $result; + + return self::apiError($result); + } + + /** + * Put method of API. + * + * @param string $url + * @param array|object $data + * @param array $headers example: array("key1:value1", "key2:value2") + * @access public + * @return object + */ + public static function apiPut($url, $data, $headers = array()) + { + $result = json_decode(commonModel::http($url, $data, array(CURLOPT_CUSTOMREQUEST => 'PUT'), $headers, 'json', 20)); + if($result && $result->code == 200) return $result; + + return self::apiError($result); + } + + /** + * Delete method of API. + * + * @param string $url + * @param array|object $data + * @param array $headers example: array("key1:value1", "key2:value2") + * @access public + * @return object + */ + public static function apiDelete($url, $data, $headers = array()) + { + $result = json_decode(commonModel::http($url, $data, array(CURLOPT_CUSTOMREQUEST => 'DELETE'), $headers, 'json', 20)); + if($result && $result->code == 200) return $result; + + return self::apiError($result); + } + + /** + * Return error object of api server. + * + * @param object|null $result + * @access protected + * @return object + */ + static protected function apiError($result) + { + global $lang; + + if($result && $result->code) return $result; + + $error = new stdclass; + $error->code = 600; + $error->message = $lang->error->httpServerError; + return $error; + } } class common extends commonModel diff --git a/module/instance/config.php b/module/instance/config.php new file mode 100644 index 0000000000..6d15555810 --- /dev/null +++ b/module/instance/config.php @@ -0,0 +1,19 @@ +instance = new stdclass; +$config->instance->keepDomainList = array(); +$config->instance->keepDomainList['console'] = 'console'; +$config->instance->keepDomainList['demo'] = 'demo'; +$config->instance->keepDomainList['s3'] = 's3'; +$config->instance->keepDomainList['s3-api'] = 's3-api'; + +$config->instance->seniorChartList = array(); +$config->instance->seniorChartList['zentao'] = ['zentao-biz', 'zentao-max']; +$config->instance->seniorChartList['zentao-biz'] = ['zentao-max']; + +$config->instance->adminer = new stdclass; +$config->instance->adminer->dbTypes = array(); +$config->instance->adminer->dbTypes['mysql'] = 'mysql'; +$config->instance->adminer->dbTypes['postgresql'] = 'pgsql'; + +$features = explode(',', getenv('QUICKON_FEATURES', '')); +$config->instance->enableAutoRestore = in_array('auto-rollback', $features); diff --git a/module/instance/control.php b/module/instance/control.php new file mode 100644 index 0000000000..ace6bbf474 --- /dev/null +++ b/module/instance/control.php @@ -0,0 +1,844 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.com + */ +class instance extends control +{ + /** + * Construct function. + * + * @param string $moduleName + * @param string $methodName + * @access public + * @return void + */ + public function __construct($moduleName = '', $methodName = '') + { + parent::__construct($moduleName, $methodName); + $this->loadModel('action'); + $this->loadModel('cne'); + $this->loadModel('store'); + } + + /** + * Show instance view. + * + * @param int $id + * @param int $recTotal + * @param int $recPerPage + * @param int $page + * @access public + * @return void + */ + public function view($id, $recTotal = 0, $recPerPage = 20, $pageID = 1, $tab ='baseinfo' ) + { + $this->loadModel('system'); + $this->app->loadLang('system'); + + $instance = $this->instance->getByID($id); + if(empty($instance)) return print(js::alert($this->lang->instance->instanceNotExists) . js::locate($this->createLink('space', 'browse'))); + + $instance = $this->instance->freshStatus($instance); + + $instanceMetric = $this->cne->instancesMetrics(array($instance)); + $instanceMetric = $instanceMetric[$instance->id]; + $this->lang->switcherMenu = $this->instance->getSwitcher($instance); + + $this->app->loadClass('pager', true); + $pager = new pager($recTotal, $recPerPage, $pageID); + + $backupList = array(); + $latestBackup = new stdclass; + if($tab == 'backup') $backupList = $this->instance->backupList($instance); + if(count($backupList)) $latestBackup = reset($backupList); + + $hasRestoreLog = false; + foreach($backupList as $backup) + { + $backup->latest_restore_time = 0; + $backup->latest_restore_status = ''; + foreach($backup->restores as $restore) + { + $hasRestoreLog = true; + if($restore->create_time > $backup->latest_restore_time) + { + $backup->latest_restore_time = $restore->create_time; + $backup->latest_restore_status = $restore->status; + } + } + } + + $dbList = new stdclass; + $currentResource = new stdclass; + $customItems = array(); + if($tab == 'advance') + { + $dbList = $this->cne->appDBList($instance); + $currentResource = $this->cne->getAppConfig($instance); + $customItems = $this->cne->getCustomItems($instance); + } + + $this->view->title = $instance->appName; + $this->view->instance = $instance; + $this->view->cloudApp = $this->loadModel('store')->getAppInfoByChart($instance->chart, $instance->channel, false); + $this->view->seniorAppList = $tab == 'baseinfo' ? $this->instance->seniorAppList($instance, $instance->channel) : array(); + $this->view->logs = $this->action->getList('instance', $id, 'date desc', $pager); + $this->view->defaultAccount = $this->cne->getDefaultAccount($instance); + $this->view->instanceMetric = $instanceMetric; + $this->view->currentResource = $currentResource; + $this->view->customItems = $customItems; + $this->view->backupList = $backupList; + $this->view->hasRestoreLog = $hasRestoreLog; + $this->view->latestBackup = $latestBackup; + $this->view->dbList = $dbList; + $this->view->domain = $this->cne->getDomain($instance); + $this->view->tab = $tab; + $this->view->pager = $pager; + + $this->display(); + } + + /** + * Display or save auto backup settings. + * + * @param int $instanceID + * @access public + * @return void + */ + public function backupSettings($instanceID) + { + $instance = $this->instance->getByID($instanceID); + + if($_POST) + { + $this->instance->saveAutoBackupSettings($instance); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $backupSettings = $this->instance->getAutoBackupSettings($instanceID); + + $startTime = strtotime($backupSettings->backupTime); + if($startTime < time()) $startTime = strtotime("+1 day $backupSettings->backupTime"); + + if($backupSettings->autoBackup) + { + $startBackupMessage = sprintf($this->lang->instance->backup->firstStartTime, $instance->name, date('Y-m-d H:i:s', $startTime)); + return $this->send(array('result' => 'success', 'message' => $startBackupMessage)); + } + + return $this->send(array('result' => 'success', 'message' => $this->lang->instance->backup->disableAutoBackup)); + } + + $backupSettings = $this->instance->getAutoBackupSettings($instanceID); + + $this->view->title = $this->lang->instance->backup->autoBackup; + $this->view->instance = $instance; + $this->view->backupSettings = $backupSettings; + + $this->display(); + } + + /** + * Display or save auto restore settings. + * + * @param int $instanceID + * @access public + * @return void + */ + public function restoreSettings($instanceID) + { + $instance = $this->instance->getByID($instanceID); + + if($_POST) + { + $this->instance->saveAutoRestoreSettings($instance); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $restoreSettings = $this->instance->getAutoRestoreSettings($instanceID); + + $startTime = strtotime($restoreSettings->restoreTime); + if($startTime < time()) $startTime = strtotime("+1 day $restoreSettings->restoreTime"); + + if($restoreSettings->autoRestore) + { + $startRestoreMessage = sprintf($this->lang->instance->restore->firstStartTime, $instance->name, date('Y-m-d H:i:s', $startTime)); + return $this->send(array('result' => 'success', 'message' => $startRestoreMessage)); + } + + return $this->send(array('result' => 'success', 'message' => $this->lang->instance->restore->disableAutoRestore)); + } + + $this->view->title = $this->lang->instance->restore->autoRestore; + $this->view->instance = $instance; + $this->view->restoreSettings = $this->instance->getAutoRestoreSettings($instanceID); + + $this->display(); + } + + /** + * Cron task of auto backup. + * + * @param string $key + * @access public + * @return void + */ + public function autoBackup($key) + { + if($this->config->instance->enableAutoRestore) return; // Only one of auto backup and auto restore can be enabled. + + $this->app->saveLog('Run auto backup at: ' . date('Y-md-d H:i:s')); + + if($key != helper::readKey()) return; + + $this->instance->autoBackup(); + } + + /** + * Cron task of auto restore. + * + * @param string $key + * @access public + * @return void + */ + public function autoRestore($key) + { + if(!$this->config->instance->enableAutoRestore) return; // Only one of auto backup and auto restore can be enabled. + + $this->app->saveLog('Run auto restore at: ' . date('Y-md-d H:i:s')); + + if($key != helper::readKey()) return; + + $this->instance->autoRestore(); + } + + /** + * Edit instance app name. + * + * @param int $id + * @access public + * @return void + */ + public function editName($id) + { + $instance = $this->instance->getByID($id); + + if(!empty($_POST)) + { + $newInstance = fixer::input('post')->trim('name')->get(); + $this->instance->updateByID($id, $newInstance); + if(dao::isError()) + { + $this->action->create('instance', $instance->id, 'editname', '', json_encode(array('result' => array('result' => 'fail'), 'data' => array('oldName' => $instance->name, 'newName' => $newInstance->name)))); + return $this->send(array('result' => 'fail', 'message' => dao::getError())); + } + + $this->action->create('instance', $instance->id, 'editname', '', json_encode(array('result' => array('result' => 'success'), 'data' => array('oldName' => $instance->name, 'newName' => $newInstance->name)))); + return print(js::closeModal('parent.parent', 'this', "function(){parent.parent.location.reload();}")); + } + + $this->view->title = $instance->name; + $this->view->instance = $instance; + + $this->display(); + } + + /** + * Upgrade to senior serial. + * + * @param int $instanceID + * @param int $seniorAppID + * @param string $confirm + * @access public + * @return void + */ + public function toSenior($instanceID, $seniorAppID, $confirm = 'no') + { + $instance = $this->instance->getByID($instanceID); + $cloudApp = $this->store->getAppInfo($seniorAppID, $instance->channel, false); + if(empty($cloudApp)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->noAppInfo)); + + if($confirm == 'yes') + { + $success = $this->instance->upgradeToSenior($instance, $cloudApp); + if($success) $this->send(array('result' => 'success', 'message' => '', 'locate' => $this->inLink('view', "id=$instance->id"))); + + $this->send(array('result' => 'fail', 'message' => dao::getError())); + } + + $this->view->title = $this->lang->instance->upgradeToSenior; + $this->view->instance = $instance; + $this->view->cloudApp = $cloudApp; + + $this->display(); + } + + /** + * Upgrade instnace + * + * @param int $id + * @access public + * @return mixed + */ + public function upgrade($id) + { + $instance = $this->instance->getByID($id); + $instance->latestVersion = $this->store->appLatestVersion($instance->appID, $instance->version); + + if($_POST) + { + if(empty($instance->latestVersion)) $this->send(array('result' => 'fail', 'message' => $this->lang->instance->noHigherVersion)); + + $postData = fixer::input('post')->get(); + + if($postData->confirm == 'yes') $success = $this->instance->upgrade($instance, $instance->latestVersion->version, $instance->latestVersion->app_version); + + $logExtra = array('result' => 'success', 'data' => array('oldVersion' => $instance->appVersion, 'newVersion' => $instance->latestVersion->app_version)); + if(!$success) + { + $logExtra['result'] = 'fail'; + $this->action->create('instance', $instance->id, 'upgrade', '', json_encode($logExtra)); + $this->send(array('result' => 'fail', 'message' => $this->lang->instance->notices['upgradeFail'])); + } + + $this->action->create('instance', $instance->id, 'upgrade', '', json_encode($logExtra)); + $this->send(array('result' => 'success', 'message' => $this->lang->instance->notices['upgradeSuccess'], 'locate' => $this->createLink('space', 'browse'), 'target' => '_self')); + } + + $this->view->title = $this->lang->instance->upgrade . $instance->name; + $this->view->instance = $instance; + + $this->display(); + } + + /** + * (Not used at present.) Install app by custom settings. + * + * @param int $id + * @access public + * @return void + */ + public function customInstall($id) + { + // Disable custom installation in version 1.0. + $storeUrl = $this->createLink('store', 'appview', "id=$id"); + return js::execute("window.parent.location.href='{$storeUrl}';"); + + $cloudApp = $this->sotre->getAppInfo($id); + if(empty($cloudApp)) return print(js::locate('back', 'parent')); + + $components = $this->store->getAppSettings($id); + + if(!empty($_POST)) + { + $postSettings = fixer::input('post')->get(); + foreach($postSettings as $key => $value) if(strpos($key, 'replicas') !== false && $value < 1) $this->send(array('result' => 'fail', 'message' => $this->lang->instance->caplicasTooSmall)); + + if(!$this->instance->install($cloudApp, $postSettings)) return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'installFail'))); + + return $this->send(array('result'=>'success', 'message' => '', 'locate' => $this->createLink('space', 'browse'))); + } + + $this->lang->switcherMenu = $this->instance->getCustomInstallSwitcher($cloudApp); + + $this->view->position[] = $this->lang->instance->customInstall; + + $this->view->title = $this->lang->instance->customInstall; + $this->view->activeTab = isset($components[0]) ? $components[0]->name : ''; + $this->view->components = $components; + $this->view->appID = $id; + + $this->display(); + } + + /** + * Install app. + * + * @param int $appID + * @access public + * @return void + */ + public function install($appID) + { + $cloudApp = $this->store->getAppInfo($appID); + if(empty($cloudApp)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->noAppInfo)); + + if(empty($this->config->demoAccounts)) + { + $clusterResource = $this->cne->cneMetrics(); + $freeMemory = intval($clusterResource->metrics->memory->allocatable * 0.9); // Remain 10% memory for system. + if($cloudApp->memory > $freeMemory) + { + $this->view->cloudApp = $cloudApp; + $this->view->gapMemory = helper::formatKB(intval(($cloudApp->memory - $freeMemory) / 1024)); + $this->view->requiredMemory = helper::formatKB(intval($cloudApp->memory / 1024)); + $this->view->freeMemory = helper::formatKB(intval($freeMemory / 1024)); + + return $this->display('instance','resourceerror'); + } + } + + $versionList = $this->store->appVersionList($cloudApp->id); + $mysqlList = $this->cne->sharedDBList('mysql'); + $pgList = $this->cne->sharedDBList('postgresql'); + $customData = new stdclass; + if(!empty($_POST)) + { + $customData = fixer::input('post') + ->trim('customName')->setDefault('customName', '') + ->trim('customDomain')->setDefault('customDomain', null) + ->trim('version')->setDefault('version', '') + ->trim('dbType')->setDefault('dbType', 'unsharedDB') + ->trim('dbService') + ->setDefault('app_version', '') + ->get(); + if($customData->version && isset($versionList[$customData->version])) $customData->app_version = $versionList[$customData->version]->app_version; + + if(isset($this->config->instance->keepDomainList[$customData->customDomain]) || $this->instance->domainExists($customData->customDomain)) return $this->send(array('result' => 'fail', 'message' => $customData->customDomain . $this->lang->instance->errors->domainExists)); + + if(!validater::checkLength($customData->customDomain, 20, 2)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->domainLength)); + if(!validater::checkREG($customData->customDomain, '/^[a-z\d]+$/')) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->wrongDomainCharacter)); + + /* If select the version, replace the latest version of App by selected version. */ + if($customData->version) + { + $cloudApp->version = $customData->version; + $cloudApp->app_version = $customData->app_version; + } + + $sharedDB = new stdclass; + if(isset($cloudApp->dependencies->mysql) && $customData->dbType == 'sharedDB') + { + $sharedDB = zget($mysqlList, $customData->dbService); + } + elseif(isset($cloudApp->dependencies->postgresql) && $customData->dbType == 'sharedDB') + { + $sharedDB = zget($pgList, $customData->dbService); + } + $instance = $this->instance->install($cloudApp, $sharedDB, $customData); + if(!$instance) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->notices['installFail'])); + + unset($_GET['onlybody']); + $this->send(array('result' => 'success', 'message' => $this->lang->instance->notices['installSuccess'], 'locate' => $this->createLink('instance', 'view', "id=$instance->id", '', false))); + } + + $this->lang->switcherMenu = $this->instance->getInstallSwitcher($cloudApp); + + $this->view->position[] = $this->view->title; + + $this->view->title = $this->lang->instance->install . $cloudApp->alias; + $this->view->cloudApp = $cloudApp; + + $this->view->versionList = array(); + foreach($versionList as $version) $this->view->versionList[$version->version] = $version->app_version . " ({$version->version})"; + + $this->view->thirdDomain = $this->instance->randThirdDomain(); + $this->view->mysqlList = $this->instance->dbListToOptions($mysqlList); + $this->view->pgList = $this->instance->dbListToOptions($pgList); + + $this->display(); + } + + /** + * Uninstall app instance. + * + * @param int $instanceID + * @access public + * @return void + */ + public function ajaxUninstall($instanceID) + { + $instance = $this->instance->getByID($instanceID); + if(!$instance) return $this->send(array('result' => 'success', 'message' => $this->lang->instance->notices['success'])); + + $success = $this->instance->uninstall($instance); + $this->action->create('instance', $instance->id, 'uninstall', '', json_encode(array('result' => $success, 'app' => array('alias' => $instance->appName, 'app_version' => $instance->version)))); + if($success) return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'uninstallSuccess'), 'locate' => $this->createLink('space', 'browse'))); + + return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'uninstallFail'))); + } + + /** + * Start app instance. + * + * @param int $instanceID + * @access public + * @return void + */ + public function ajaxStart($instanceID) + { + $instance = $this->instance->getByID($instanceID); + if(!$instance) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->instanceNotExists)); + + $result = $this->instance->start($instance); + $this->action->create('instance', $instance->id, 'start', '', json_encode(array('result' => $result, 'app' => array('alias' => $instance->appName, 'app_version' => $instance->version)))); + + if($result->code == 200) return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'startSuccess'))); + + return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'startFail'))); + } + + /** + * Stop app instance. + * + * @param int $instanceID + * @access public + * @return void + */ + public function ajaxStop($instanceID) + { + $instance = $this->instance->getByID($instanceID); + if(!$instance) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->instanceNotExists)); + + $result = $this->instance->stop($instance); + $this->action->create('instance', $instance->id, 'stop', '', json_encode(array('result' => $result, 'app' => array('alias' => $instance->appName, 'app_version' => $instance->version)))); + if($result->code == 200) return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'stopSuccess'))); + + return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'stopFail'))); + } + + /** + * Query status of app instance. + * + * @access public + * @return void + */ + public function ajaxStatus() + { + $postData = fixer::input('post')->setDefault('idList', array())->get(); + + $instances = $this->instance->getByIdList($postData->idList); + $statusList = $this->instance->batchFresh($instances); + + return $this->send(array('result' => 'success', 'data' => $statusList)); + } + + /** + * Backup instnacd by ajax. + * + * @param int $instanceID + * @access public + * @return void + */ + public function ajaxBackup($instanceID) + { + $instance = $this->instance->getByID($instanceID); + $success = $this->instance->backup($instance, $this->app->user); + if(!$success) + { + $this->action->create('instance', $instance->id, 'backup', '', json_encode(array('result' => array('result' => 'fail')))); + return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'backupFail'))); + } + + $this->action->create('instance', $instance->id, 'backup', '', json_encode(array('result' => array('result' => 'success')))); + return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'backupSuccess'))); + } + + /** + * Restore instance by ajax + * + * @access public + * @return void + */ + public function ajaxRestore() + { + $postData = fixer::input('post') + ->trim('instanceID') + ->trim('backupName')->get(); + + if(empty($postData->instanceID) || empty($postData->backupName)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->wrongRequestData)); + + $instance = $this->instance->getByID($postData->instanceID); + if(empty($instance))return print(js::alert($this->lang->instance->instanceNotExists) . js::locate($this->createLink('space', 'browse'))); + + $success = $this->instance->restore($instance, $this->app->user, $postData->backupName); + if(!$success) + { + $this->action->create('instance', $instance->id, 'restore', '', json_encode(array('result' => array('result' => 'fail')))); + return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'restoreFail'))); + } + + $this->action->create('instance', $instance->id, 'restore', '', json_encode(array('result' => array('result' => 'success')))); + return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'restoreSuccess'))); + } + + /** + * Delete backup by ajax. + * + * @param int $backupID + * @access public + * @return void + */ + public function ajaxDeleteBackup($backupID) + { + $success = $this->instance->deleteBackup($backupID, $this->app->user); + if(!$success) return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'deleteFail'))); + + return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'deleteSuccess'))); + } + + /** + * Generate database auth parameters and jump to login page. + * + * @access public + * @return void + */ + public function ajaxDBAuthUrl() + { + $post = fixer::input('post') + ->setDefault('namespace', 'default') + ->setDefault('instanceID', 0) + ->setDefault('dbType', '') + ->get(); + if(empty($post->dbName)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->dbNameIsEmpty)); + + $instance = $this->instance->getByID($post->instanceID); + if(empty($instance)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->instanceNotExists)); + + $detail = $this->loadModel('cne')->appDBDetail($instance, $post->dbName); + if(empty($detail)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->notFoundDB)); + + $dbAuth = array(); + $dbAuth['driver'] = zget($this->config->instance->adminer->dbTypes, $post->dbType, ''); + $dbAuth['server'] = $detail->host . ':' . $detail->port; + $dbAuth['username'] = $detail->username; + $dbAuth['db'] = $detail->database; + $dbAuth['password'] = $detail->password; + + $url = '/adminer?' . http_build_query($dbAuth); + $this->send(array('result' => 'success', 'message' => '', 'data' => array('url' => $url))); + } + + /** + * Adjust instance memory size by ajax. + * + * @param int $instanceID + * @access public + * @return void + */ + public function ajaxAdjustMemory($instanceID) + { + $postData = fixer::input('post')->get(); + + /* Check free memory size is enough or not. */ + $clusterResource = $this->cne->cneMetrics(); + $freeMemory = intval($clusterResource->metrics->memory->allocatable * 0.9); // Remain 10% memory for system. + if($postData->memory_kb * 1024 > $freeMemory) $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->notEnoughResource)); + + /* Request CNE to adjust memory size. */ + $instance = $this->instance->getByID($instanceID); + if(!$this->instance->updateMemorySize($instance, $postData->memory_kb * 1024)) + { + $this->send(array('result' => 'fail', 'message' => dao::getError())); + } + + $this->send(array('result' => 'success', 'message' => '')); + } + + /** + * Switch LDAP between enable and disable by ajax. + * + * @param int $instanceID + * @access public + * @return void + */ + public function ajaxSwitchLDAP($instanceID) + { + $instance = $this->instance->getByID($instanceID); + $postData = fixer::input('post')->get(); + + if($this->instance->switchLDAP($instance, $postData->enableLDAP == 'true')) + { + $this->send(array('result' => 'success', 'message' => '')); + } + + $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->switchLDAPFailed)); + } + + /** + * Switch SMTP between enable and disable by ajax. + * + * @param int $instanceID + * @access public + * @return void + */ + public function ajaxSwitchSMTP($instanceID) + { + $instance = $this->instance->getByID($instanceID); + $postData = fixer::input('post')->get(); + + if($this->instance->switchSMTP($instance, $postData->enableSMTP == 'true')) + { + $this->send(array('result' => 'success', 'message' => '')); + } + + $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->switchSMTPFailed)); + } + + /** + * Update custom settings by ajax. For example: env variables. + * + * @param int $instanceID + * @access public + * @return void + */ + public function ajaxUpdateCustom($instanceID) + { + $instance = $this->instance->getByID($instanceID); + if(!$instance) $this->send(array('result' => 'fail', 'message' => $this->lang->instance->instanceNotExists)); + + $postData = fixer::input('post')->get(); + + $settings = new stdclass; + $settings->force_restart = true; + $settings->settings_map = new stdclass; + $settings->settings_map->custom = $postData; + + if($this->cne->updateConfig($instance, $settings)) + { + $this->action->create('instance', $instanceID, 'updatecustom', '', json_encode($settings)); + $this->send(array('result' => 'success', 'message' => '')); + } + + $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->setEnvFailed)); + } + + /** + * Delete expired demo instance by cron. + * + * @access public + * @return void + */ + public function deleteExpiredDemoInstance() + { + if(empty($this->config->demoAccounts)) return $this->send(array('result' => 'fail', 'message' => 'This api is only for demo enviroment.')); + + $this->instance->deleteExpiredDemoInstance(); + + $this->send(array('result' => 'success', 'message' => '')); + } + + /** + * Get instance info for q tool in console. + * + * @param int $id + * @access public + * @return mixed + */ + public function apiDetail($id) + { + if(!$this->checkCneToken()) + { + header("HTTP/1.1 401"); + return print(json_encode(array('code' => 401, 'message' => 'Invalid token.'))); + } + + if(empty($id)) return print(json_encode(array('code' => 401, 'message' => 'Invalid id.'))); + + $instance = $this->instance->getByID($id); + if(empty($instance)) return print(json_encode(array('code' => 404, 'message' => 'Not found.', 'data' => array()))); + + $instance->space = $instance->spaceData && isset($instance->spaceData->k8space) ? $instance->spaceData->k8space : ''; + unset($instance->desc); + unset($instance->spaceData); + + return print(json_encode(array('code' => 200, 'message' => 'success', 'data' => $instance))); + } + + /** + * Get instances list by account through api for q tool. + * + * @access public + * @return void + */ + public function apiBrowse() + { + if(!$this->checkCneToken()) + { + header("HTTP/1.1 401"); + return print(json_encode(array('code' => 401, 'message' => 'Invalid token.'))); + } + + $requestBody = json_decode(file_get_contents("php://input")); + $account = zget($requestBody, 'account', ''); + if(empty($account)) return print(json_encode(array('code' => 700, 'message' => 'Account is required.', 'data' => new stdclass))); + + $recPerPage = zget($requestBody, 'perPage', 20); + $pageID = zget($requestBody, 'page', 1); + + $this->app->loadClass('pager', true); + $pager = pager::init(0, $recPerPage, $pageID); + + $instanceList = $this->instance->getByAccount($account, $pager); + + $result = new stdclass; + $result->list = $instanceList; + $result->page = $pageID; + $result->perPage = $recPerPage; + $result->pageTotal = $pager->pageTotal; + $result->total = $pager->recTotal; + + return print(json_encode(array('code' => 200, 'message' => 'success', 'data' => $result))); + } + + /** + * Install app by api for q tool. + * + * @access public + * @return void + */ + public function apiInstall() + { + if(!$this->checkCneToken()) + { + header("HTTP/1.1 401"); + return print(json_encode(array('code' => 401, 'message' => 'Invalid token.'))); + } + + $requestBody = json_decode(file_get_contents("php://input")); + $chart = zget($requestBody , 'chart', ''); + if(empty($chart)) return print(json_encode(array('code' => 701, 'message' => 'Param chart is required.'))); + + $user = null; + $account = zget($requestBody, 'account', ''); + if($account) $user = $this->loadModel('user')->getById($account); + if(empty($user)) $user = $this->loadModel('company')->getAdmin(); + if(empty($user)) return print(json_encode(array('code' => 703, 'message' => 'No user.'))); + + $this->app->user = $user; + + $name = zget($requestBody , 'name', ''); + $channel = zget($requestBody , 'channel', 'stable'); + $k8name = zget($requestBody , 'k8name', ''); + if($k8name && $this->instance->k8nameExists($k8name)) return print(json_encode(array('code' => 706, 'message' => $k8name . ' has been used, please change it and try again.'))); + + $thirdDomain = zget($requestBody , 'domain', ''); + if($this->instance->domainExists($thirdDomain)) return print(json_encode(array('code' => 705, 'message' => $thirdDomain . ' has been used, please change it and try again.'))); + + $cloudApp = $this->store->getAppInfoByChart($chart, $channel, false); + if(empty($cloudApp)) return print(json_encode(array('code' => 702, 'message' => 'App not found.'))); + + $result = $this->instance->apiInstall($cloudApp, $thirdDomain, $name, $k8name, $channel); + + if($result) return print(json_encode(array('code' => 200, 'message' => 'success', 'data' => new stdclass))); + + return print(json_encode(array('code' => 704, 'message' => 'Fail to install app.', 'data' => new stdclass))); + } + + /** + * Check CNE token. + * + * @access private + * @return bool + */ + private function checkCneToken() + { + $token = zget($_SERVER, 'HTTP_TOKEN'); + return $token == $this->config->CNE->api->token; + } +} diff --git a/module/instance/css/backupsettings.css b/module/instance/css/backupsettings.css new file mode 100644 index 0000000000..4d57b80074 --- /dev/null +++ b/module/instance/css/backupsettings.css @@ -0,0 +1 @@ +#backupSettingForm{ margin-bottom: 40px;} diff --git a/module/instance/css/install.css b/module/instance/css/install.css new file mode 100644 index 0000000000..7f5d8754b9 --- /dev/null +++ b/module/instance/css/install.css @@ -0,0 +1,5 @@ +#installForm .advanced {padding: 10px 0; border-bottom: solid 1px #e4e4e4;} +#installForm .advanced a {font-weight: 700;} +#installForm .advanced .icon {display: inline-block; margin-left: 10px;} +#installForm .advanced a:hover, #installForm table td a:focus, #installForm table td a:hover {color: #61be68;} +#installForm td {height: 46px;} diff --git a/module/instance/css/upgrade.css b/module/instance/css/upgrade.css new file mode 100644 index 0000000000..dc896b7a03 --- /dev/null +++ b/module/instance/css/upgrade.css @@ -0,0 +1 @@ +.modal-message {font-weight: bold; padding: 20px;} diff --git a/module/instance/css/view.css b/module/instance/css/view.css new file mode 100644 index 0000000000..693f0a86bf --- /dev/null +++ b/module/instance/css/view.css @@ -0,0 +1,32 @@ +#mainContent .nav-tabs li>a::before {height: 0;} +.instance-name h3 {display: inline-block; padding-left: 10px;} +.instance-name > span {font-size: 14px; font-weight: bolder; padding: 3px 3px; margin: 0 5px;} +.instance-panel .btn-group .btn {width: 60px;} +.instance-panel .btn-group i {font-size: 24px;} +.instance-status span {display: inline-block; margin-right: 10px; padding-bottom: 1px;} +.instance-status i {font-size: 18px;} +.instance-source span {display: inline-block; margin-right: 10px; width: 100px;} +#senior-app-desc a {color: #61be68} +a#serialDiff {color: #ff9800;} +#seniorAppPanel span.text-info:hover {color: #2196f3;} +.instance-log .panel-body {padding-bottom: 8px; padding-left: 10px;} +.instance-log .panel-body table {margin-bottom: 0;} +.usage-box {border-radius: 3px;} +.c-title {text-align: center; padding-top: 10px; font-size: 15px} +.progress-pie {margin: 10px auto;} +.instance-panel > div > a:last-child { margin-left: 8px;} +.solution-link {display: inline-block; margin-left: 20px;} + +#backup .panel {padding: 10px;} +#backup .panel .btn-toolbar{padding: 0 0 10px 0;} +#backup .panel .panel-heading {height: 50px;} +#backup table.table {margin-bottom: 20px; width: auto;} +#backup table th.actions {padding-left: 15px;} +#backup table td>.btn {padding-left: 5px; padding-right: 5px;} +#backup .btn.btn-info[disabled] {color: rgba(255,255,255,.7); background-color: #61be68;} + +#advance .panel-title span {padding-left: 10px;} +#advance hr {margin: 5px 20px;} +#advance .scalable-input{height: 26px;} +#advance .center{text-align: center;} +#advance .hide{display: none} diff --git a/module/instance/js/backupsettings.js b/module/instance/js/backupsettings.js new file mode 100644 index 0000000000..34afcd2a27 --- /dev/null +++ b/module/instance/js/backupsettings.js @@ -0,0 +1,64 @@ +$(function() +{ + $(".form-time").datetimepicker({ + weekStart: 1, + todayBtn: 1, + autoclose: 1, + todayHighlight: 1, + startView: 1, + minView: 0, + maxView: 1, + forceParse: 0, + format: 'hh:ii' + }); + + $("input[type=checkbox][name='autoBackup[]']").on('change', function(event) + { + var enabled = $("input[type=checkbox][name='autoBackup[]']:checked").length > 0; + if(enabled) + { + $('.backup-settings').show(); + } + else + { + $('.backup-settings').hide(); + } + }); + + $("input[type=checkbox][name='autoBackup[]']").change(); + + $("#keepDays").on('input', function(event) + { + console.log(event,event.target.value); + event.target.value = event.target.value.replace(/[^\d]+/g,''); + if(Number(event.target.value) < 1) + { + event.target.value = 1; + } + + if(Number(event.target.value) > 30) + { + event.target.value = 30; + } + }); + + $('#backupSettingForm #saveSetting').on('click', function() + { + var settings = $('#backupSettingForm').serialize(); + $.post(createLink('instance', 'backupSettings', 'id=' + instanceID), settings).done(function(response) + { + var res = JSON.parse(response); + + if(res.result == 'success') + { + parent.window.$.closeModal(); + parent.window.bootAlert({title: instanceNotices.success, message:res.message}); + } + else + { + var errMsg = res.message instanceof Array ? res.message.join('
') : res.message; + parent.window.bootAlert({title: instanceNotices.fail, message: errMsg}); + } + }); + }); +}); diff --git a/module/instance/js/custominstall.js b/module/instance/js/custominstall.js new file mode 100644 index 0000000000..777a9bad9d --- /dev/null +++ b/module/instance/js/custominstall.js @@ -0,0 +1,7 @@ +$(function() +{ + $('input[type=number]').on('change', function(event) + { + if($(event.target).val() <= 1) $(event.target).val(1); + }); +}); diff --git a/module/instance/js/install.js b/module/instance/js/install.js new file mode 100644 index 0000000000..9871b0da8d --- /dev/null +++ b/module/instance/js/install.js @@ -0,0 +1,48 @@ +$(function() +{ + $('#installForm').on('submit', function(event) + { + event.preventDefault(); + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.installing + '
', + }); + + $.post($('#installForm').attr('action'), $('#installForm').serializeArray()).done(function(response) + { + loadingDialog.modal('hide'); + + let res = JSON.parse(response); + if(res.result == 'success') + { + config.onlybody = 'no'; + window.parent.$.apps.open(res.locate, 'space'); + } + else + { + alert(res.message); + } + }); + }); + + $("input[type=radio][name='dbType']").on('change', function(event) + { + if(event.target.value == 'sharedDB') + { + $("select[name=dbService]").closest('td').show(); + } + else + { + $("select[name=dbService]").closest('td').hide(); + } + }); + + $(".advanced a").on('click', function(event) + { + let downArrow = $(".advanced a .icon-chevron-double-down"); + let upArrow = $(".advanced a .icon-chevron-double-up"); + if(downArrow.length >0 ) downArrow.removeClass('icon-chevron-double-down').addClass('icon-chevron-double-up'); + if(upArrow.length >0 ) upArrow.removeClass('icon-chevron-double-up').addClass('icon-chevron-double-down'); + }); +}); diff --git a/module/instance/js/restoresettings.js b/module/instance/js/restoresettings.js new file mode 100644 index 0000000000..3363a9043c --- /dev/null +++ b/module/instance/js/restoresettings.js @@ -0,0 +1,49 @@ +$(function() +{ + $(".form-time").datetimepicker({ + weekStart: 1, + todayBtn: 1, + autoclose: 1, + todayHighlight: 1, + startView: 1, + minView: 0, + maxView: 1, + forceParse: 0, + format: 'hh:ii' + }); + + $("input[type=checkbox][name='autoRestore[]']").on('change', function(event) + { + var enabled = $("input[type=checkbox][name='autoRestore[]']:checked").length > 0; + if(enabled) + { + $('.restore-settings').show(); + } + else + { + $('.restore-settings').hide(); + } + }); + + $("input[type=checkbox][name='autoRestore[]']").change(); + + $('#restoreSettingForm #saveSetting').on('click', function() + { + var settings = $('#restoreSettingForm').serialize(); + $.post(createLink('instance', 'restoreSettings', 'id=' + instanceID), settings).done(function(response) + { + var res = JSON.parse(response); + + if(res.result == 'success') + { + parent.window.$.closeModal(); + parent.window.bootAlert({title: instanceNotices.success, message:res.message}); + } + else + { + var errMsg = res.message instanceof Array ? res.message.join('
') : res.message; + parent.window.bootAlert({title: instanceNotices.fail, message: errMsg}); + } + }); + }); +}); diff --git a/module/instance/js/tosenior.js b/module/instance/js/tosenior.js new file mode 100644 index 0000000000..7d38f3d821 --- /dev/null +++ b/module/instance/js/tosenior.js @@ -0,0 +1,14 @@ +$(function(){ + $("#toSeniorForm input#readtrue").on('change', function() + { + if($('#toSeniorForm input#readtrue:checked').length > 0) + { + $('#toSeniorForm button[type=submit]').attr('disabled', false); + } + else + { + $('#toSeniorForm button[type=submit]').attr('disabled', true); + } + }); + $("#toSeniorForm input#readtrue").change(); +}); diff --git a/module/instance/js/upgrade.js b/module/instance/js/upgrade.js new file mode 100644 index 0000000000..ee3d82b731 --- /dev/null +++ b/module/instance/js/upgrade.js @@ -0,0 +1,30 @@ +$(function() +{ + $('#upgradeForm').on('submit', function(event) + { + event.preventDefault(); + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.upgrading + '
', + }); + + $.post($('#upgradeForm').attr('action'), $('#upgradeForm').serializeArray()).done(function(response) + { + loadingDialog.modal('hide'); + window.parent.$('#triggerModal').modal('hide'); + + let res = JSON.parse(response); + if(res.result == 'success') + { + config.onlybody = 'no'; + window.parent.$.apps.reload('space', createLink('space', 'browse'), 'space'); + window.parent.location.reload(); + } + else + { + alert(res.message); + } + }); + }); +}); diff --git a/module/instance/js/view.js b/module/instance/js/view.js new file mode 100644 index 0000000000..84ab6378df --- /dev/null +++ b/module/instance/js/view.js @@ -0,0 +1,502 @@ +$(function() +{ + $('#memBtn').on('click', function(event) + { + bootbox.confirm(instanceNotices.adjustMemory, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.adjusting + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'ajaxAdjustMemory', 'id=' + id, 'json'); + var adjustData = {memory_kb: $('select[name=memory_kb]').val()}; + $.post(url, adjustData).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + window.parent.$.apps.open(createLink('instance', 'view', 'id=' + id), 'space'); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + + }); + }); + + $('#ldapBtn').on('click', function(event) + { + bootbox.confirm(instanceNotices.switchLDAP, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.switching + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'ajaxSwitchLDAP', 'id=' + id, 'json'); + var postData = {enableLDAP: $("[name='enableLDAP[]']:checked").length > 0}; + $.post(url, postData).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + window.parent.location.reload(); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('#smtpBtn').on('click', function(event) + { + bootbox.confirm(instanceNotices.switchSMTP, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.switching + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'ajaxSwitchSMTP', 'id=' + id, 'json'); + var postData = {enableSMTP: $("[name='enableSMTP[]']:checked").length > 0}; + $.post(url, postData).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + window.parent.location.reload(); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('#customBtn').on('click', function(event) + { + var errors = ''; + $("#customForm").find("input[type=text]").each(function(index, item) + { + if($(item).val().trim().length == 0) + { + errors += $(item).attr('placeholder') + instanceNotices.required + '
'; + } + }); + if(errors.length > 0) + { + bootbox.alert( + { + title: instanceNotices.error, + message: errors, + }); + return; + } + + bootbox.confirm(instanceNotices.confirmCustom, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.setting + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'ajaxUpdateCustom', 'id=' + id, 'json'); + var formData = $('#customForm').serializeArray(); + var customData = {}; + formData.forEach(function(item, index){ + customData[item.name] = item.value; + }); + + $.post(url, customData).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + window.parent.location.reload(); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + + }); + }); + + $('.btn-uninstall').on('click', function(event) + { + bootbox.confirm(instanceNotices.confirmUninstall, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.uninstalling + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'ajaxUninstall', 'id=' + id, 'json'); + $.post(url).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + window.parent.$.apps.open(createLink('space', 'browse'), 'space'); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('.btn-start').on('click', function(event) + { + bootbox.confirm(instanceNotices.confirmStart, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.starting + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'ajaxStart', 'id=' + id, 'json'); + $.post(url).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + window.location.reload(); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('.btn-stop').on('click', function(event) + { + bootbox.confirm(instanceNotices.confirmStop, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.stopping + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'ajaxStop', 'id=' + id, 'json'); + $.post(url).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + window.location.reload(); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('.btn-backup').on('click', function(event) + { + $('#confirmRestore').modal('hide'); + bootbox.confirm(instanceNotices.confirmBackup, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.backuping + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'ajaxBackup', 'id=' + id, 'json'); + $.post(url).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + bootbox.alert( + { + title: instanceNotices.success, + message: res.message, + callback: function(){window.location.reload();} + }); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('.btn-restore').click(function() + { + let backupName = $(this).attr('backup-name'); + let instanceID = $(this).attr('instance-id'); + $('#confirmRestore #submitRestore').data('backup-name', backupName); + $('#confirmRestore #submitRestore').data('instance-id', instanceID); + $('#confirmRestore').modal('show'); + }); + + $('#submitRestore').on('click', function(event) + { + $('#confirmRestore').modal('hide'); + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.restoring + '
', + }); + + var instanceID = $(event.target).closest('button').data('instance-id'); + var backupName = $(event.target).closest('button').data('backup-name'); + var url = createLink('instance', 'ajaxRestore', '', 'json'); + $.post(url, { instanceID, backupName }).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + bootbox.alert( + { + title: instanceNotices.success, + message: res.message, + callback: function(){window.location.reload();} + }); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + + $('.btn-delete-backup').on('click', function(event) + { + bootbox.confirm(instanceNotices.confirmDelete, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.deleting + '
', + }); + + var id = $(event.target).closest('button').attr('backup-id'); + var url = createLink('instance', 'ajaxDeleteBackup', 'id=' + id, 'json'); + $.post(url).done(function(response) + { + loadingDialog.modal('hide'); + + var res = JSON.parse(response); + if(res.result == 'success') + { + bootbox.alert( + { + title: instanceNotices.success, + message: res.message, + callback: function(){window.location.reload();} + }); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('button.db-login').on('click', function(event) + { + var dbName = $(event.target).data('db-name'); + var dbType = $(event.target).data('db-type'); + var instanceID = $(event.target).data('id'); + + $.post(createLink('instance', 'ajaxDBAuthUrl'), {dbName, dbType, instanceID}).done(function(res) + { + var response = JSON.parse(res); + if(response.result == 'success') + { + window.parent.open(response.data.url, 'Adminer'); + } + else + { + bootbox.alert(response.message); + } + }); + }); + + var enableTimer = true; + window.parent.$(window.parent.document).on('showapp', function(event, app) + { + enableTimer = app.code == 'space'; + }); + + setInterval(function() + { + if(!enableTimer) return; + + var statusURL = createLink('instance', 'ajaxStatus'); + $.post(statusURL, {idList: instanceIdList}).done(function(response) + { + var res = JSON.parse(response); + if(res.result == 'success' && res.data instanceof Array) + { + res.data.forEach(function(instance) + { + if($(".instance-status[instance-id=" + instance.id + "]").data('status') != instance.status) window.location.reload(); + }); + } + if(res.locate) window.parent.location.href = res.locate; + }); + }, 1000 * 5); + + $('[data-toggle="tooltip"]').tooltip(); + + /* Count down for demo instance. */ + setInterval(function() + { + var nowSeconds = Math.round((new Date).getTime() / 1000); + $('.count-down').each(function(index, item) + { + var createdAt = $(item).data('created-at'); + var passSeconds = nowSeconds - createdAt; + var leftSeconds = (demoAppLife ? demoAppLife : 30) * 60 - passSeconds; + + if(leftSeconds < 0) + { + window.parent.location.href = createLink('space', 'browse'); + } + else + { + var minutes = Math.floor(leftSeconds / 60); + var seconds = Math.round(leftSeconds % 60); + $(item).find('.left-time').text(('0' + minutes).slice(-2) + ':' + ('0' + seconds).slice(-2)); + } + + }) + }, 1000) + + $('#replicas-edit').on('click', function() + { + $('#replicas-input').show() + $('#replicas-text').hide() + $('#replicas-save').show() + $('#replicas-edit').hide() + }) + + $('#replicas-save').on('click', function() + { + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.adjusting + '
', + }); + + var id = $(event.target).closest('button').attr('instance-id'); + var url = createLink('instance', 'replicas', 'id=' + id, 'json'); + var adjustData = {replicas: $('#replicas-input').val()}; + $.post(url, adjustData).done(function(response) + { + + var res = JSON.parse(response); + if(res.result == 'success') + { + window.parent.location.reload(); + } + else + { + loadingDialog.modal('hide'); + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }) +}) diff --git a/module/instance/lang/zh-cn.php b/module/instance/lang/zh-cn.php new file mode 100644 index 0000000000..2782d4a305 --- /dev/null +++ b/module/instance/lang/zh-cn.php @@ -0,0 +1,329 @@ +instance = new stdclass; +$lang->instance->name = '名称'; +$lang->instance->appName = '应用模板'; +$lang->instance->version = '版本'; +$lang->instance->status = '状态'; +$lang->instance->cpu = 'CPU'; +$lang->instance->mem = '内存'; +$lang->instance->space = '空间'; +$lang->instance->domain = '域名'; +$lang->instance->visitIP = '访问方式'; +$lang->instance->dbType = '数据库'; +$lang->instance->advanceOption = '高级选项'; +$lang->instance->baseInfo = '基本信息'; +$lang->instance->backupAndRestore = '备份'; +$lang->instance->advance = '高级'; +$lang->instance->enableLDAP = '启用LDAP'; +$lang->instance->linkLDAP = '集成LDAP'; +$lang->instance->enableSMTP = '启用SMTP'; +$lang->instance->customSetting = '自定义配置'; +$lang->instance->upgradeToSenior = '升级到高级版'; +$lang->instance->updateDomain = '更新域名'; +$lang->instance->start = '启动'; +$lang->instance->restart = '重启'; +$lang->instance->stop = '关闭'; +$lang->instance->install = '安装'; +$lang->instance->update = '更新'; +$lang->instance->upgrade = '升级'; +$lang->instance->customInstall = '自定义安装'; +$lang->instance->uninstall = '卸载'; +$lang->instance->visit = '访问'; +$lang->instance->editName = '修改名称'; +$lang->instance->cpuCore = '核'; +$lang->instance->scalable = '应用水平扩容'; +$lang->instance->change = '修改'; + +$lang->instance->systemLDAPInactive = '未开启系统LDAP'; +$lang->instance->toSystemLDAP = '去启用'; + +$lang->instance->enableSMTP = '启用SMTP'; +$lang->instance->systemSMTPInactive = '未开启系统SMTP'; +$lang->instance->toSystemSMTP = '去启用'; + + +$lang->instance->serviceInfo = '服务信息'; +$lang->instance->appTemplate = '应用模板'; +$lang->instance->source = '来源'; +$lang->instance->installAt = '部署时间'; +$lang->instance->runDuration = '已运行'; +$lang->instance->defaultAccount = '默认用户'; +$lang->instance->defaultPassword = '默认密码'; +$lang->instance->operationLog = '操作记录'; +$lang->instance->installedService = '已安装服务'; +$lang->instance->runningService = '运行中的服务'; +$lang->instance->installApp = '安装应用'; +$lang->instance->cpuUsage = 'CPU'; +$lang->instance->memUsage = '内存'; +$lang->instance->currentMemory = '当前内存'; +$lang->instance->adjustMem = '调整内存'; +$lang->instance->saveSetting = '保存设置'; +$lang->instance->leftTime = '剩余'; +$lang->instance->switchTo = '升级到'; +$lang->instance->or = '或'; +$lang->instance->hasRead = '已阅读'; +$lang->instance->stopInstanceTips = '关闭服务后才能升级到高级版!'; + +$lang->instance->dbTypes = array(); +$lang->instance->dbTypes['sharedDB'] = '共享数据库'; +$lang->instance->dbTypes['unsharedDB'] = '独享数据库'; + +$lang->instance->backup = new stdclass; +$lang->instance->backup->common = '备份'; +$lang->instance->backup->date = '备份时间'; +$lang->instance->backup->operator = '备份人'; +$lang->instance->backup->type = '备份类型'; +$lang->instance->backup->backupStatus = '备份状态'; +$lang->instance->backup->restoreStatus = '回滚状态'; +$lang->instance->backup->restoreOperator = '回滚人'; +$lang->instance->backup->restoreTime = '回滚时间'; +$lang->instance->backup->action = '操作'; +$lang->instance->backup->restore = '回滚'; +$lang->instance->backup->restoreInfo = '回滚信息'; +$lang->instance->backup->delete = '删除'; +$lang->instance->backup->rebackup = '重试备份'; +$lang->instance->backup->create = '创建备份'; +$lang->instance->backup->database = '数据库'; +$lang->instance->backup->dbType = '类型'; +$lang->instance->backup->dbName = '名称'; +$lang->instance->backup->dbStatus = '状态'; +$lang->instance->backup->dbSpentSeconds = '耗时(秒)'; +$lang->instance->backup->dbSize = '大小'; +$lang->instance->backup->volumne = '数据卷'; +$lang->instance->backup->volName = '名称'; +$lang->instance->backup->volMountName = '挂载目录'; +$lang->instance->backup->volStatus = '状态'; +$lang->instance->backup->volSpentSeconds = '耗时(秒)'; +$lang->instance->backup->volSize = '大小'; +$lang->instance->backup->lastRestore = '上次回滚'; +$lang->instance->backup->restoreDate = '回滚时间'; +$lang->instance->backup->latestBackupAt = '上次备份时间'; +$lang->instance->backup->backupBeforeRestore = '回滚前建议您先备份!'; +$lang->instance->backup->enableAutoBackup = '开启自动备份'; +$lang->instance->backup->autoBackup = '自动备份'; +$lang->instance->backup->cycleDays = '备份周期'; +$lang->instance->backup->backupTime = '备份时间'; +$lang->instance->backup->keepDays = '保留天数'; +$lang->instance->backup->keepDayRange = '请输入1~30之间的整数'; +$lang->instance->backup->firstStartTime = '%s 首次备份将于%s 执行'; +$lang->instance->backup->invalidTime = '无效的时间'; +$lang->instance->backup->disableAutoBackup = '自动备份已关闭'; +$lang->instance->backup->keepBackupBySystem = '备份数据超过1条时系统才会删除过期的自动备份数据。'; + +$lang->instance->backup->cycleList[1] = '每日'; + +$lang->instance->backup->operators = array(); +$lang->instance->backup->operators['auto'] = '自动备份'; + +$lang->instance->backup->statusList = array(); +$lang->instance->backup->statusList['success'] = '成功'; +$lang->instance->backup->statusList['failed'] = '失败'; +$lang->instance->backup->statusList['pending'] = '等待中'; +$lang->instance->backup->statusList['processing'] = '备份中'; +$lang->instance->backup->statusList['inprogress'] = '备份中'; +$lang->instance->backup->statusList['completed'] = '完成'; +$lang->instance->backup->statusList['executedFailed'] = '失败'; +$lang->instance->backup->statusList['uploading'] = '上传中'; +$lang->instance->backup->statusList['deleting'] = '删除中'; +$lang->instance->backup->statusList['uploadFailed'] = '上传失败'; +$lang->instance->backup->statusList['downloading'] = '下载中'; +$lang->instance->backup->statusList['downloadFailed'] = '下载失败'; + +$lang->instance->restore = new stdclass; +$lang->instance->restore->autoRestore = '自动还原'; +$lang->instance->restore->enableAutoRestore = '开启自动还原'; +$lang->instance->restore->cycleDays = '还原周期'; +$lang->instance->restore->restoreTime = '还原时间'; +$lang->instance->restore->invalidTime = '无效的时间'; +$lang->instance->restore->disableAutoRestore = '自动还原已关闭'; +$lang->instance->restore->firstStartTime = '%s 首次自动还原将于%s 执行'; + +$lang->instance->restore->cycleList[1] = '每日'; + +$lang->instance->restore->statusList = array(); +$lang->instance->restore->statusList['pending'] = '等待中'; +$lang->instance->restore->statusList['inprogress'] = '进行中'; +$lang->instance->restore->statusList['completed'] = '完成'; +$lang->instance->restore->statusList['failed'] = '失败'; + +$lang->instance->dbList = '数据库'; +$lang->instance->dbName = '名称'; +$lang->instance->dbStatus = '状态'; +$lang->instance->dbType = '类型'; +$lang->instance->action = '操作'; +$lang->instance->management = '管理'; +$lang->instance->dbReady = '正常'; +$lang->instance->dbWaiting = '等待就绪'; + +$lang->instance->log = new stdclass; +$lang->instance->log->date = '日期'; +$lang->instance->log->message = '内容'; + +$lang->instance->actionList = array(); +$lang->instance->actionList['install'] = '安装了%s'; +$lang->instance->actionList['uninstall'] = '卸载了%s'; +$lang->instance->actionList['start'] = '启动了%s'; +$lang->instance->actionList['stop'] = '关闭了%s'; +$lang->instance->actionList['editname'] = '修改了名称'; +$lang->instance->actionList['upgrade'] = '升级了%s'; +$lang->instance->actionList['backup'] = '备份了%s'; +$lang->instance->actionList['restore'] = '回滚了%s'; +$lang->instance->actionList['adjustmemory'] = '调整 %s 的内存到 %s'; +$lang->instance->actionList['adjustreplicas'] = '调整 %s 的副本数到 %s'; +$lang->instance->actionList['enableldap'] = '启用了LDAP'; +$lang->instance->actionList['disableldap'] = '禁用了LDAP'; +$lang->instance->actionList['enablesmtp'] = '启用了SMTP'; +$lang->instance->actionList['disablesmtp'] = '禁用了SMTP'; +$lang->instance->actionList['updatecustom'] = '修改了自定义配置'; +$lang->instance->actionList['tosenior'] = '升级服务到高级版'; +$lang->instance->actionList['saveautobackupsettings'] = '修改了自动备份设置'; +$lang->instance->actionList['saveautorestoresettings'] = '修改了自动还原设置'; +$lang->instance->actionList['autobackup'] = '系统执行了自动备份'; +$lang->instance->actionList['autorestore'] = '系统执行了自动还原'; +$lang->instance->actionList['deleteexpiredbackup'] = '系统删除了过期的自动备份'; + +$lang->instance->sourceList = array(); +$lang->instance->sourceList['cloud'] = '渠成公共市场'; +$lang->instance->sourceList['local'] = '本地市场'; + +$lang->instance->channelList = array(); +$lang->instance->channelList['test'] = '测试版'; +$lang->instance->channelList['stable'] = '稳定版'; + +$lang->instance->statusList = array(); +$lang->instance->statusList['installationFail'] = '安装失败'; +$lang->instance->statusList['creating'] = '创建中'; +$lang->instance->statusList['initializing'] = '初始化'; +$lang->instance->statusList['pulling'] = '下载中'; +$lang->instance->statusList['startup'] = '启动中'; +$lang->instance->statusList['starting'] = '启动中'; +$lang->instance->statusList['running'] = '运行中'; +$lang->instance->statusList['suspending'] = '暂停中'; +$lang->instance->statusList['suspended'] = '已暂停'; +$lang->instance->statusList['installing'] = '安装中'; +$lang->instance->statusList['uninstalling'] = '卸载中'; +$lang->instance->statusList['stopping'] = '关闭中'; +$lang->instance->statusList['stopped'] = '已关闭'; +$lang->instance->statusList['destroying'] = '销毁中'; +$lang->instance->statusList['destroyed'] = '已销毁'; +$lang->instance->statusList['abnormal'] = '异常'; +$lang->instance->statusList['upgrading'] = '更新中'; +$lang->instance->statusList['unknown'] = '未知'; + +$lang->instance->htmlStatuses = array(); +$lang->instance->htmlStatuses['running'] = "%s"; +$lang->instance->htmlStatuses['stopped'] = "%s"; +$lang->instance->htmlStatuses['abnormal'] = "%s"; +$lang->instance->htmlStatuses['installationFail'] = $lang->instance->htmlStatuses['abnormal']; +$lang->instance->htmlStatuses['busy'] = "%s"; + +$lang->instance->memOptions = array(); +$lang->instance->memOptions[128 * 1024] = '128MB'; +$lang->instance->memOptions[256 * 1024] = '256MB'; +$lang->instance->memOptions[512 * 1024] = '512MB'; +$lang->instance->memOptions[1024 * 1024] = '1GB'; +$lang->instance->memOptions[2048 * 1024] = '2GB'; +$lang->instance->memOptions[4096 * 1024] = '4GB'; +$lang->instance->memOptions[8192 * 1024] = '8GB'; +$lang->instance->memOptions[16384 * 1024] = '16GB'; +$lang->instance->memOptions[32768 * 1024] = '32GB'; + +$lang->instance->componentFields = array(); +$lang->instance->componentFields['replicas'] = '副本数'; +$lang->instance->componentFields['cpu_limit'] = 'CPU'; +$lang->instance->componentFields['mem_limit'] = '内存'; + +$lang->instance->notices = array(); +$lang->instance->notices['success'] = '成功'; +$lang->instance->notices['fail'] = '失败'; +$lang->instance->notices['error'] = '错误'; +$lang->instance->notices['confirmStart'] = '确定启动该应用吗?'; +$lang->instance->notices['confirmStop'] = '确定关闭该应用吗?'; +$lang->instance->notices['confirmUninstall'] = '确定卸载该应用吗?'; +$lang->instance->notices['startSuccess'] = '启动成功'; +$lang->instance->notices['startFail'] = '启动失败'; +$lang->instance->notices['stopSuccess'] = '关闭成功'; +$lang->instance->notices['stopFail'] = '关闭失败'; +$lang->instance->notices['uninstallSuccess'] = '卸载成功'; +$lang->instance->notices['uninstallFail'] = '卸载失败'; +$lang->instance->notices['installSuccess'] = '安装成功'; +$lang->instance->notices['installFail'] = '安装失败'; +$lang->instance->notices['upgradeSuccess'] = '升级成功'; +$lang->instance->notices['upgradeFail'] = '升级失败'; +$lang->instance->notices['backupSuccess'] = '备份任务已提交'; +$lang->instance->notices['backupFail'] = '备份失败'; +$lang->instance->notices['restoreSuccess'] = '回滚任务已提交'; +$lang->instance->notices['restoreFail'] = '回滚失败'; +$lang->instance->notices['deleteSuccess'] = '删除成功'; +$lang->instance->notices['deleteFail'] = '删除失败'; +$lang->instance->notices['starting'] = '启动中,请稍候...'; +$lang->instance->notices['stopping'] = '关闭中,请稍候...'; +$lang->instance->notices['installing'] = '安装中,请稍候...'; +$lang->instance->notices['uninstalling'] = '卸载中,请稍候...'; +$lang->instance->notices['upgrading'] = '升级中,请稍候...'; +$lang->instance->notices['backuping'] = '备份中,请稍候...'; +$lang->instance->notices['restoring'] = '回滚中,请稍候...'; +$lang->instance->notices['deleting'] = '删除中,请稍候...'; +$lang->instance->notices['adjusting'] = '调整中,请稍候...'; +$lang->instance->notices['switching'] = '提交中,请稍候...'; +$lang->instance->notices['setting'] = '提交中,请稍候...'; +$lang->instance->notices['confirmInstall'] = '确定要安装(%s)?'; +$lang->instance->notices['confirmUpgrade'] = '确定要升级到最新版吗?'; +$lang->instance->notices['confirmBackup'] = '确定要备份吗?'; +$lang->instance->notices['confirmRestore'] = '本操作将用备份的数据覆盖当前的数据,确定要回滚吗?'; +$lang->instance->notices['confirmDelete'] = '确定要删除该备份数据吗?'; +$lang->instance->notices['adjustMemory'] = '确定要调整中内存吗?'; +$lang->instance->notices['switchLDAP'] = '修改LDAP会重启服务,确定要修改LDAP吗?'; +$lang->instance->notices['enableLDAPFailed'] = '启用LDAP失败'; +$lang->instance->notices['disableLDAPFailed'] = '禁用LDAP失败'; +$lang->instance->notices['enableLDAPSuccess'] = '启用LDAP成功'; +$lang->instance->notices['disableLDAPSuccess'] = '禁用LDAP成功'; +$lang->instance->notices['switchSMTP'] = '修改SMTP会重启服务,确定要修改SMTP吗?'; +$lang->instance->notices['enableSMTPFailed'] = '启用SMTP失败'; +$lang->instance->notices['disableSMTPFailed'] = '禁用SMTP失败'; +$lang->instance->notices['enableSMTPSuccess'] = '启用SMTP成功'; +$lang->instance->notices['disableSMTPSuccess'] = '禁用SMTP成功'; +$lang->instance->notices['confirmCustom'] = '修改自定义配置后服务将自动重启以使配置生效。'; +$lang->instance->notices['required'] = '不能为空'; + +$lang->instance->nameChangeTo = ' %s 修改为 %s 。'; +$lang->instance->versionChangeTo = ' %s 升级为 %s 。'; +$lang->instance->toSeniorSerial = '从 %s 升级到 %s 。'; +$lang->instance->adjustMemorySize = '建议调整内存到 %s 。'; +$lang->instance->enableAutoBackup = '开启自动备份'; +$lang->instance->disableAutoBackup = '关闭自动备份'; + +$lang->instance->instanceNotExists = '服务不存在'; +$lang->instance->caplicasTooSmall = '副本数不能小于1'; +$lang->instance->empty = '暂无服务'; +$lang->instance->noComponent = '无组件,点击'; +$lang->instance->noHigherVersion = '未找到更高版本!'; +$lang->instance->backupOnlyRunning = '运行状态才能备份'; +$lang->instance->restoreOnlyRunning = '运行状态才能回滚'; +$lang->instance->howToSelectDB = '如何选择?'; +$lang->instance->appLifeTip = 'demo账号安装的应用有30分钟限制,30分钟后自动删除。'; +$lang->instance->serialDiff = '查看版本区别'; +$lang->instance->descOfSwitchSerial = '您当前使用的是%s,想要体验更多高级功能,可升级至%s。'; +$lang->instance->toSeniorAttention = '重要提示'; +$lang->instance->toSeniorTips = ""; + +$lang->instance->errors = new stdclass; +$lang->instance->errors->domainLength = '域名长度必须介于2-20字符之间'; +$lang->instance->errors->domainExists = '域名已被占用,请使用其它域名。'; +$lang->instance->errors->wrongDomainCharacter = '域名只能是小写英文字母和数字'; +$lang->instance->errors->noAppInfo = '获取应用数据失败,请稍候重试。'; +$lang->instance->errors->notEnoughResource = '集群资源不足'; +$lang->instance->errors->notEnoughMemory = '%s 应用需要 %s 内存,当前可用内存%s ,还需要 %s,请扩充内存资源或卸载其他服务后重试!'; +$lang->instance->errors->restoreRunning = '当前回滚正在进行中,请等待当前回滚完成。'; +$lang->instance->errors->noBackup = '无备份数据,'; +$lang->instance->errors->wrongRequestData = '提交的数据有误,请刷新页面后重试。'; +$lang->instance->errors->noDBList = '无数据库或不可访问'; +$lang->instance->errors->notFoundDB = '找不到该数据库'; +$lang->instance->errors->dbNameIsEmpty = '数据库名为空'; +$lang->instance->errors->failToAdjustMemory = '调整内存失败'; +$lang->instance->errors->switchLDAPFailed = '修改LDAP设置失败'; +$lang->instance->errors->switchSMTPFailed = '修改SMTP设置失败'; +$lang->instance->errors->updateCustomFailed = '修改自定义配置失败'; +$lang->instance->errors->failToSenior = '升级到高级版失败'; +$lang->instance->errors->failedToUpdateDomain = '更新域名失败'; diff --git a/module/instance/model.php b/module/instance/model.php new file mode 100644 index 0000000000..fd400b36eb --- /dev/null +++ b/module/instance/model.php @@ -0,0 +1,2059 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.com + */ +class InstanceModel extends model +{ + /** + * Construct method: load CNE model, and set primaryDomain. + * + * @access public + * @return void + */ + public function __construct() + { + parent::__construct(); + $this->loadModel('cne'); + $this->loadModel('action'); + } + + /** + * Get by id. + * + * @param int $id + * @access public + * @return object|null + */ + public function getByID($id) + { + $deadline = date('Y-m-d H:i:s', strtotime("-{$this->config->demoAppLife} minutes")); + + $instance = $this->dao->select('*')->from(TABLE_INSTANCE) + ->where('id')->eq($id) + ->andWhere('deleted')->eq(0) + ->fetch(); + if(!$instance) return null; + + $instance->spaceData = $this->dao->select('*')->from(TABLE_SPACE)->where('id')->eq($instance->space)->fetch(); + if($instance->solution) + { + $solution = $this->dao->select('*')->from(TABLE_SOLUTION)->where('id')->eq($instance->solution)->fetch(); + $instance->solutionData = $solution; + } + + return $instance; + } + + /** + * Get by id list. + * + * @param array $idList + * @access public + * @return array + */ + public function getByIdList($idList) + { + $deadline = date('Y-m-d H:i:s', strtotime("-{$this->config->demoAppLife} minutes")); + + $instances = $this->dao->select('*')->from(TABLE_INSTANCE) + ->where('id')->in($idList) + ->andWhere('deleted')->eq(0) + ->fetchAll('id'); + + $spaces = $this->dao->select('*')->from(TABLE_SPACE)->where('deleted')->eq(0)->andWhere('id')->in(array_column($instances, 'space'))->fetchAll('id'); + foreach($instances as $instance) $instance->spaceData = zget($spaces, $instance->space, new stdclass); + + $solutionIDList = array_column($instances, 'solution'); + $solutions = $this->dao->select('*')->from(TABLE_SOLUTION)->where('id')->in($solutionIDList)->fetchAll('id'); + foreach($instances as $instance) $instance->solutionData = zget($solutions, $instance->solution, new stdclass); + + return $instances; + } + + /** + * Get instance list by solution. + * + * @param object $solution + * @param string $chart + * @access public + * @return array + */ + public function instanceOfSolution($solution, $chart) + { + $instance = $this->dao->select('id')->from(TABLE_INSTANCE)->where('deleted')->eq(0) + ->andWhere('solution')->eq($solution->id) + ->andWhere('chart')->eq($chart) + ->fetch(); + + if($instance) return $this->getByID($instance->id); + + return; + } + + /** + * Get instances list by account. + * + * @param string $account + * @param object $pager + * @access public + * @return array + */ + public function getByAccount($account = '', $pager = null, $pinned = '', $searchParam = '', $status = 'all') + { + $deadline = date('Y-m-d H:i:s', strtotime("-{$this->config->demoAppLife} minutes")); + + $defaultSpace = $this->loadModel('space')->defaultSpace($account ? $account : $this->app->user->account); + + $instances = $this->dao->select('instance.*')->from(TABLE_INSTANCE)->alias('instance') + ->leftJoin(TABLE_SPACE)->alias('space')->on('space.id=instance.space') + ->where('instance.deleted')->eq(0) + ->andWhere('space.id')->eq($defaultSpace->id) + ->beginIF($account)->andWhere('space.owner')->eq($account)->fi() + ->beginIF($pinned)->andWhere('instance.pinned')->eq((int)$pinned)->fi() + ->beginIF($searchParam)->andWhere('instance.name')->like("%{$searchParam}%")->fi() + ->beginIF($status != 'all')->andWhere('instance.status')->eq($status)->fi() + ->orderBy('instance.id desc') + ->beginIF($pager)->page($pager)->fi() + ->fetchAll('id'); + + $spaces = $this->dao->select('*')->from(TABLE_SPACE) + ->where('deleted')->eq(0) + ->andWhere('id')->in(array_column($instances, 'space')) + ->fetchAll('id'); + + foreach($instances as $instance) $instance->spaceData = zget($spaces, $instance->space, new stdclass); + + $solutionIDList = array_column($instances, 'solution'); + $solutions = $this->dao->select('*')->from(TABLE_SOLUTION)->where('id')->in($solutionIDList)->fetchAll('id'); + foreach($instances as $instance) $instance->solutionData = zget($solutions, $instance->solution, new stdclass); + + return $instances; + } + + /** + * Get instance list that has been enabled LDAP. + * + * @access public + * @return array + */ + public function getListEnabledLDAP() + { + $instances = $this->dao->select('*')->from(TABLE_INSTANCE)->where('deleted')->eq(0)->andWhere('length(ldapSnippetName) > 0')->fetchAll('id'); + + $spaces = $this->dao->select('*')->from(TABLE_SPACE) + ->where('deleted')->eq(0) + ->andWhere('id')->in(array_column($instances, 'space')) + ->fetchAll('id'); + + foreach($instances as $instance) $instance->spaceData = zget($spaces, $instance->space, new stdclass); + + return $instances; + } + + /** + * Count instance which is enabled LDAP. + * + * @access public + * @return int + */ + public function countLDAP() + { + $count = $this->dao->select('count(*) as ldapQty')->from(TABLE_INSTANCE)->where('deleted')->eq(0)->andWhere('length(ldapSnippetName) > 0')->fetch(); + return $count->ldapQty; + } + + /** + * Count instance which is enabled SMTP. + * + * @access public + * @return int + */ + public function countSMTP() + { + $count = $this->dao->select('count(*) as Qty')->from(TABLE_INSTANCE)->where('deleted')->eq(0)->andWhere('length(smtpSnippetName) > 0')->fetch(); + return $count->Qty; + } + + /** + * Get quantity of total installed services. + * + * @access public + * @return int + */ + public function totalServices() + { + $deadline = date('Y-m-d H:i:s', strtotime("-{$this->config->demoAppLife} minutes")); + + $defaultSpace = $this->loadModel('space')->defaultSpace($this->app->user->account); + + $count = $this->dao->select('count(*) as qty')->from(TABLE_INSTANCE)->alias('instance') + ->leftJoin(TABLE_SPACE)->alias('space')->on('space.id=instance.space') + ->where('instance.deleted')->eq(0) + ->andWhere('space.id')->eq($defaultSpace->id) + ->fetch(); + + return $count->qty; + } + + /** + * Pin instance to navigation page or Unpin instance. + * + * @param int $instanceID + * @access public + * @return void + */ + public function pinToggle($instanceID) + { + $instance = $this->getByID($instanceID); + $pinned = $instance->pinned == '0' ? '1' : '0'; + $this->dao->update(TABLE_INSTANCE)->set('pinned')->eq($pinned)->where('id')->eq($instanceID)->exec(); + } + + /** + * Switch LDAP between enable and disable. + * + * @param object $instance + * @param bool $enableSMTP + * @access public + * @return bool + */ + public function switchSMTP($instance, $enableSMTP) + { + $this->loadModel('system'); + $snippetName = $this->system->smtpSnippetName(); + + $settings = new stdclass; + if($enableSMTP) + { + $settings->settings_snippets = [$snippetName]; + } + else + { + $settings->settings_snippets = [$snippetName . '-']; + } + + $success = $this->cne->updateConfig($instance, $settings); + if(!$success) + { + dao::$errors[] = $this->lang->instance->errors->failToAdjustMemory; + return false; + } + + if($enableSMTP) + { + $this->dao->update(TABLE_INSTANCE)->set('smtpSnippetName')->eq($snippetName)->where('id')->eq($instance->id)->exec(); + $this->action->create('instance', $instance->id, 'enableSMTP'); + } + else + { + $this->dao->update(TABLE_INSTANCE)->set('smtpSnippetName')->eq('')->where('id')->eq($instance->id)->exec(); + $this->action->create('instance', $instance->id, 'disableSMTP'); + } + + return true; + } + + /** + * Switch LDAP between enable and disable. + * + * @param object $instance + * @param bool $enableLDAP + * @access public + * @return bool + */ + public function switchLDAP($instance, $enableLDAP) + { + $this->loadModel('system'); + $snippetName = $this->system->ldapSnippetName(); + + $settings = new stdclass; + $settings->force_restart = true; + if($enableLDAP) + { + $settings->settings_snippets = [$snippetName]; + } + else + { + $settings->settings_snippets = [$snippetName . '-']; + } + + $success = $this->cne->updateConfig($instance, $settings); + if(!$success) + { + dao::$errors[] = $this->lang->instance->errors->failToAdjustMemory; + return false; + } + + if($enableLDAP) + { + $this->dao->update(TABLE_INSTANCE)->set('ldapSnippetName')->eq($snippetName)->where('id')->eq($instance->id)->exec(); + $this->action->create('instance', $instance->id, 'enableLDAP'); + } + else + { + $this->dao->update(TABLE_INSTANCE)->set('ldapSnippetName')->eq('')->where('id')->eq($instance->id)->exec(); + $this->action->create('instance', $instance->id, 'disableLDAP'); + } + + return true; + } + + /** + * Count old domain. + * + * @access public + * @return int + */ + public function countOldDomain() + { + /* REPLACE(domain, .$sysDomain, '') used for special case: new domain is a.com and old domain is b.a.com, domain of instance is c.b.a.com, */ + $sysDomain = $this->loadModel('cne')->sysDomain(); + $result = $this->dao->select('count(*) as qty')->from(TABLE_INSTANCE) + ->where('deleted')->eq(0) + ->andWhere("REPLACE(domain, '.$sysDomain', '')")->like("%.%") + ->fetch(); + + return $result->qty; + } + + /** + * Update all instances domain by customized domain. + * + * @access public + * @return void + */ + public function updateInstancesDomain() + { + /* REPLACE(domain, .$sysDomain, '') used for special case: new domain is a.com and old domain is b.a.com, domain of instance is c.b.a.com, new domain of instance should be c.a.com */ + $sysDomain = $this->loadModel('cne')->sysDomain(); + $instanceList = $this->dao->select('*')->from(TABLE_INSTANCE) + ->where('deleted')->eq(0) + ->andWhere("REPLACE(domain, '.$sysDomain', '')")->like("%.%") + ->fetchAll('id'); + + $spaces = $this->dao->select('*')->from(TABLE_SPACE)->where('deleted')->eq(0)->andWhere('id')->in(array_column($instanceList, 'space'))->fetchAll('id'); + + foreach($instanceList as $instance) $instance->spaceData = zget($spaces, $instance->space, new stdclass); + + foreach($instanceList as $instance) $this->updateDomain($instance); + } + + /** + * Update domain. + * + * @param object $instance + * @access public + * @return bool + */ + public function updateDomain($instance) + { + /* If domain of instance is same with system domain, not need to update. */ + $sysDomain = $this->cne->sysDomain(); + if(stripos(str_replace($sysDomain, '', $instance->domain), '.') === false) return true; + + $expiredDomains = $this->loadModel('setting')->getItem('owner=system&module=common§ion=domain&key=expiredDomain'); + $expiredDomains = json_decode($expiredDomains, true); + if(empty($expiredDomains)) return true; + + $leftDomain = ''; + foreach($expiredDomains as $expiredDomain) + { + if(stripos($instance->domain, $expiredDomain) === false) continue; + + $leftDomain = trim(str_replace($expiredDomain, '', $instance->domain), '.'); // Pick left domain. + /* If left domain like a.b, skip it, because left domain must be letters without dot(.) . */ + if(stripos($leftDomain, '.') !== false) continue; // Does not pick left domain. + + $newDomain = $leftDomain . '.' . $sysDomain; + + $settings = new stdclass; + $settings->settings_map = new stdclass; + $settings->settings_map->ingress = new stdclass; + $settings->settings_map->ingress->enabled = true; + $settings->settings_map->ingress->host = $newDomain; //$this->fullDomain($leftDomain); + + $settings->settings_map->global = new stdclass; + $settings->settings_map->global->ingress = $settings->settings_map->ingress; + + if($this->cne->updateConfig($instance, $settings)) + { + $this->dao->update(TABLE_INSTANCE)->set('domain')->eq($newDomain)->where('id')->eq($instance->id)->exec(); + } + } + + return true; + } + + /** + * Update instance memory size. + * + * @param object $instnace + * @param int $size + * @access public + * @return bool + */ + public function updateMemorySize($instnace, $size = '') + { + $settings = new stdclass; + $settings->settings_map = new stdclass; + $settings->settings_map->resources = new stdclass; + $settings->settings_map->resources->memory = $size; + + $success = $this->cne->updateConfig($instnace, $settings); + if($success) + { + $this->action->create('instance', $instnace->id, 'adjustMemory', helper::formatKB(intval($size / 1024))); + return true; + } + + dao::$errors[] = $this->lang->instance->errors->failToAdjustMemory; + return false; + } + + /** + * Update instance status. + * + * @param int $int + * @param string $status + * @access public + * @return int + */ + public function updateStatus($id, $status) + { + $instanceData = new stdclass; + $instanceData->status = trim($status); + return $this->updateByID($id, $instanceData); + } + + /** + * Update instance by id. + * + * @param int $id + * @param object $newInstance + * @access public + * @return void + */ + public function updateByID($id, $newInstance) + { + return $this->dao->update(TABLE_INSTANCE)->data($newInstance) + ->autoCheck() + ->checkIF(isset($newInstance->name), 'name', 'notempty') + ->checkIF(isset($newInstance->status), 'status', 'in', array_keys($this->lang->instance->statusList)) + ->where('id')->eq($id)->exec(); + } + + + /** + * Soft delete instance by id. + * + * @param int $id + * @access public + * @return void + */ + public function softDeleteByID($id) + { + return $this->dao->update(TABLE_INSTANCE)->set('deleted')->eq(1)->where('id')->eq($id)->exec(); + } + + /** + * If actions are allowed to do. + * + * @param string $action + * @param object $instance + * @access public + * @return boolean + */ + public function canDo($action, $instance) + { + $busy = in_array($instance->status, array('creating', 'initializing', 'starting', 'stopping', 'suspending', 'pulling', 'destroying')); + switch($action) + { + case 'start': + return !($busy or in_array($instance->status, array('running', 'abnormal', 'destroyed'))); + case 'stop': + return !in_array($instance->status, array('stopped', 'stopping', 'destroying', 'suspending', 'suspended')); + case 'uninstall': + return !in_array($instance->status, array('destroying')); + case 'visit': + return $instance->status == 'running' or $instance->status == 'upgrading'; + default: + return false; + } + } + + /** + * Get URL for visiting instance service. + * + * @param object $instance + * @access public + * @return string + */ + public function url($instance) + { + $url = "//" . $instance->domain; + $port = getenv('APP_HTTPS_PORT'); + if($port and $port != '443') $url .= ":$port/"; + + return $url; + } + + /** + * Create third domain. + * + * @param int $length + * @param int $triedTimes + * @access public + * @return string + */ + public function randThirdDomain($length = 4, $triedTimes = 0) + { + if($triedTimes > 16) $length++; + + $thirdDomain = strtolower(helper::randStr($length)); + if(!$this->domainExists($thirdDomain)) return $thirdDomain; + + return $this->randThirdDomain($length, $triedTimes++); + } + + /** + * Return full domain. + * + * @param string $thirdDomain + * @access public + * @return string + */ + public function fullDomain($thirdDomain) + { + return $thirdDomain . '.' . $this->cne->sysDomain(); + } + + /** + * Check if the domain exists. + * + * @param string $thirdDomain + * @access public + * @return bool true: exists, false: not exist. + */ + public function domainExists($thirdDomain) + { + $domain = $this->fullDomain($thirdDomain); + return boolval($this->dao->select('id')->from(TABLE_INSTANCE)->where('domain')->eq($domain)->andWhere('deleted')->eq(0)->fetch()); + } + + /** + * Check if the k8name exists. + * + * @param string $k8name + * @access public + * @return bool true: exists, false: not exist. + */ + public function k8nameExists($k8name) + { + return boolval($this->dao->select('id')->from(TABLE_INSTANCE)->where('k8name')->eq($k8name)->andWhere('deleted')->eq(0)->fetch()); + } + + /** + * Mount installation settings by custom data. + * + * @param object $customData + * @param object $dbInfo + * @param object $instance + * @access private + * @return object + */ + private function installationSettingsMap($customData, $dbInfo, $instance) + { + $settingsMap = new stdclass; + if($customData->customDomain) + { + $settingsMap->ingress = new stdclass; + $settingsMap->ingress->enabled = true; + $settingsMap->ingress->host = $this->fullDomain($customData->customDomain); + + $settingsMap->global = new stdclass; + $settingsMap->global->ingress = new stdclass; + $settingsMap->global->ingress->enabled = true; + $settingsMap->global->ingress->host = $settingsMap->ingress->host; + } + + if(empty($customData->dbType) || $customData->dbType == 'unsharedDB' || empty($customData->dbService)) return $settingsMap; + + /* Set DB settings. */ + $dbSettings = new stdclass; + $dbSettings->service = $dbInfo->name; + $dbSettings->namespace = $dbInfo->namespace; + $dbSettings->host = $dbInfo->host; + $dbSettings->port = $dbInfo->port; + $dbSettings->name = 'db_' . $instance->id; + $dbSettings->user = 'user_' . $instance->id; + + $dbSettings = $this->getValidDBSettings($dbSettings, $dbSettings->user, $dbSettings->name); + + $settingsMap->mysql = new stdclass; + $settingsMap->mysql->enabled = false; + + $settingsMap->mysql->auth = new stdclass; + $settingsMap->mysql->auth->user = $dbSettings->user; + $settingsMap->mysql->auth->password = helper::randStr(12); + $settingsMap->mysql->auth->host = $dbSettings->host; + $settingsMap->mysql->auth->port = $dbSettings->port; + $settingsMap->mysql->auth->database = $dbSettings->name; + + $settingsMap->mysql->auth->dbservice = new stdclass; + $settingsMap->mysql->auth->dbservice->name = $dbSettings->service; + $settingsMap->mysql->auth->dbservice->namespace = $dbSettings->namespace; + + return $settingsMap; + } + + /** + * Return valid DBSettings. + * + * @param object $dbSettings + * @param string $defaultUser + * @param string $defaultDBName + * @param int $times + * @access private + * @return null|object + */ + private function getValidDBSettings($dbSettings, $defaultUser, $defaultDBName, $times = 1) + { + if($times >10) return; + + $validatedResult = $this->cne->validateDB($dbSettings->service, $dbSettings->name, $dbSettings->user, $dbSettings->namespace); + if($validatedResult->user && $validatedResult->database) return $dbSettings; + + if(!$validatedResult->user) $dbSettings->user = $defaultUser . '_' . help::randStr(4); + if(!$validatedResult->database) $dbSettings->database = $defaultDBName . '_' . help::randStr(4); + + return $this->getValidDBSettings($dbSettings, $defaultUser, $defaultDBName, $times++); + } + + /** + * Install app by request from Web page. + * + * @param object $app + * @param object $dbInfo + * @param object $customData + * @param int $spaceID + * @access public + * @return false|object Failure: return false, Success: return instance + */ + public function install($app, $dbInfo, $customData, $spaceID = null, $settings = array()) + { + $this->loadModel('space'); + if($spaceID) + { + $space = $this->space->getByID($spaceID); + } + else + { + $space = $this->space->defaultSpace($this->app->user->account); + } + + $snippets = array(); + if(isset($customData->ldapSnippet[0])) $snippets['ldapSnippetName'] = $customData->ldapSnippet[0]; + if(isset($customData->smtpSnippet[0])) $snippets['smtpSnippetName'] = $customData->smtpSnippet[0]; + + $channel = $this->app->session->cloudChannel ? $this->app->session->cloudChannel : $this->config->cloud->api->channel; + $instance = $this->createInstance($app, $space, $customData->customDomain, $customData->customName, '', $channel, $snippets); + if(!$instance) return false; + + $settingMap = $this->installationSettingsMap($customData, $dbInfo, $instance); + return $this->doCneInstall($instance, $space, $settingMap, $snippets, $settings); + } + + /** + * Install System SLB component. + * + * @param object $app + * @param string $k8name + * @param string $channel + * @access public + * @return object + */ + public function installSysSLB($app, $k8name = 'cne-lb', $channel = 'stable') + { + $this->app->loadLang('system'); + + $space = $this->loadModel('space')->getSystemSpace($this->app->user->account); + + $instance = $this->createInstance($app, $space, '', '', $k8name, $channel); + if(!$instance) + { + dao::$errors[] = $this->lang->system->errors->failToInstallLDAP; + return false; + } + + $instance = $this->doCneInstall($instance, $space, (new stdclass)); + if(!$instance) + { + dao::$errors[] = $this->lang->system->errors->failToInstallLDAP; + return false; + } + + return $instance; + } + + /** + install global SMTP proxy service. + * + * @param object $app + * @param object $smtpSettings + * @param string $instanceName + * @param string $k8name + * @param string $channel + * @access public + * @return object + */ + public function installSysSMTP($app, $smtpSettings, $instanceName = '', $k8name = '', $channel = 'stable') + { + $this->app->loadLang('system'); + + $space = $this->loadModel('space')->getSystemSpace($this->app->user->account); + + $customData = new stdclass; + $customData->dbType = null; + $customData->customDomain = $this->randThirdDomain(); + + $instance = $this->createInstance($app, $space, $customData->customDomain, $instanceName, $k8name, $channel); + if(!$instance) + { + dao::$errors[] = $this->lang->system->errors->failToInstallSMTP; + return false; + } + + $settingsMap = $this->installationSettingsMap($customData, array(), $instance); + + $settingsMap->env = new stdclass; + $settingsMap->env->SMTP_HOST = $smtpSettings->host; + $settingsMap->env->SMTP_PORT = strval($smtpSettings->port); + $settingsMap->env->SMTP_USER = $smtpSettings->user; + $settingsMap->env->SMTP_PASS = $smtpSettings->pass; + $settingsMap->env->AUTHENTICATE_CODE = helper::randStr(24); + + $instance = $this->doCneInstall($instance, $space, $settingsMap); + if(!$instance) + { + dao::$errors[] = $this->lang->system->errors->failToInstallSMTP; + return false; + } + + /* Post snippet to CNE. */ + $snippetSettings = new stdclass; + $snippetSettings->name = 'snippet-smtp-proxy'; + $snippetSettings->namespace = $space->k8space; + $snippetSettings->auto_import = false; + + $snippetSettings->values = new stdclass; + $snippetSettings->values->mail = new stdclass; + $snippetSettings->values->mail->enabled = true; + $snippetSettings->values->mail->smtp = new stdclass; + $snippetSettings->values->mail->smtp->host = "{$instance->k8name}.{$snippetSettings->namespace}.svc"; + $snippetSettings->values->mail->smtp->port = '1025'; + $snippetSettings->values->mail->smtp->user = 'smtp-bot@quickon.local'; // This is fake value. + $snippetSettings->values->mail->smtp->pass = $settingsMap->env->AUTHENTICATE_CODE; // This is fake value. + + $snippetResult = $this->loadModel('cne')->addSnippet($snippetSettings); + if($snippetResult->code != 200) + { + dao::$errors[] = $this->lang->system->errors->failToInstallSMTP; + $this->uninstall($instance); + return false; + } + + /* Save SMTP settings. */ + $secretKey = helper::readKey(); + $settingsMap->env->SMTP_PASS = openssl_encrypt($settingsMap->env->SMTP_PASS, 'DES-ECB', $secretKey); + $settingsMap->env->AUTHENTICATE_CODE = openssl_encrypt($settingsMap->env->AUTHENTICATE_CODE, 'DES-ECB', $secretKey); + + $snippetSettings->values->mail->smtp->pass = $settingsMap->env->AUTHENTICATE_CODE; + + $this->loadModel('setting'); + $this->setting->setItem('system.common.smtp.enabled', true); + $this->setting->setItem('system.common.smtp.instanceID', $instance->id); + $this->setting->setItem('system.common.smtp.snippetName', $snippetSettings->name); + $this->setting->setItem('system.common.smtp.settingsMap', json_encode($settingsMap)); + $this->setting->setItem('system.common.smtp.snippetSettings', json_encode($snippetSettings)); + + return $instance; + } + + /** + * Install LDAP. + * + * @param object $app + * @param string $thirdDomain + * @param string $instanceName + * @param string $channel + * @access public + * @return bool|object + */ + public function installLDAP($app, $thirdDomain = '', $instanceName = '', $k8name = '', $channel = 'stable') + { + $this->app->loadLang('system'); + + $space = $this->loadModel('space')->getSystemSpace($this->app->user->account); + + $customData = new stdclass; + $customData->dbType = null; + $customData->customDomain = $thirdDomain ? $thirdDomain : $this->randThirdDomain(); + + $instance = $this->createInstance($app, $space, $customData->customDomain, $instanceName, $k8name, $channel); + if(!$instance) + { + dao::$errors[] = $this->lang->system->errors->failToInstallLDAP; + return false; + } + + $settingMap = $this->installationSettingsMap($customData, array(), $instance); + + $settingMap->auth = new stdclass; + $settingMap->auth->username = $app->account ? $app->account->username : 'admin'; + $settingMap->auth->password = helper::randStr(16); + $settingMap->auth->root = 'dc=quickon,dc=org'; + + $instance = $this->doCneInstall($instance, $space, $settingMap); + if(!$instance) + { + dao::$errors[] = $this->lang->system->errors->failToInstallLDAP; + return false; + } + + /* Post snippet to CNE. */ + $snippetSettings = new stdclass; + $snippetSettings->name = 'snippet-qucheng-ldap'; + $snippetSettings->namespace = $space->k8space; + $snippetSettings->auto_import = false; + + $snippetSettings->values = new stdclass; + $snippetSettings->values->auth = new stdclass; + $snippetSettings->values->auth->ldap = new stdclass; + $snippetSettings->values->auth->ldap->enabled = true; + $snippetSettings->values->auth->ldap->type = 'ldap'; + $snippetSettings->values->auth->ldap->host = "{$instance->k8name}.{$snippetSettings->namespace}.svc"; + $snippetSettings->values->auth->ldap->port = '1389'; + $snippetSettings->values->auth->ldap->bindDN = "cn={$settingMap->auth->username},dc=quickon,dc=org"; + $snippetSettings->values->auth->ldap->bindPass = $settingMap->auth->password; + $snippetSettings->values->auth->ldap->baseDN = $settingMap->auth->root; + $snippetSettings->values->auth->ldap->filter = "&(objectClass=posixAccount)(cn=%s)"; + $snippetSettings->values->auth->ldap->attrUser = 'uid'; + $snippetSettings->values->auth->ldap->attrEmail = 'mail'; + + $snippetResult = $this->loadModel('cne')->addSnippet($snippetSettings); + if($snippetResult->code != 200) + { + dao::$errors[] = $this->lang->system->errors->failToInstallLDAP; + $this->uninstall($instance); + return false; + } + + /* Save LDAP account. */ + $secretKey = helper::readKey(); + $settingMap->auth->password = openssl_encrypt($settingMap->auth->password, 'DES-ECB', $secretKey); + $this->dao->update(TABLE_INSTANCE)->set('ldapSettings')->eq(json_encode($settingMap))->where('id')->eq($instance->id)->exec(); + $this->loadModel('setting')->setItem('system.common.ldap.active', 'qucheng'); + $this->loadModel('setting')->setItem('system.common.ldap.instanceID', $instance->id); + $this->loadModel('setting')->setItem('system.common.ldap.snippetName', $snippetSettings->name); // Parameter for App installation API. + + return $instance; + } + + /** + * Install app through API. + * + * @param object $cloudApp + * @param string $thirdDomain + * @param string $name + * @param string $channel + * @access public + * @return bool|object + */ + public function apiInstall($cloudApp, $thirdDomain = '', $name = '', $k8name = '', $channel = 'stable') + { + $this->loadModel('space'); + $space = $this->space->defaultSpace($this->app->user->account); + + $customData = new stdclass; + $customData->dbType = null; + $customData->customDomain = $thirdDomain ? $thirdDomain : $this->randThirdDomain(); + + $dbInfo = new stdclass; + $dbList = $this->cne->sharedDBList(); + if(count($dbList) > 0) + { + $dbInfo = reset($dbList); + + $customData->dbType = 'sharedDB'; + $customData->dbService = $dbInfo->name; // Use first shared database. + } + + $instance = $this->createInstance($cloudApp, $space, $customData->customDomain, $name, $k8name, $channel); + if(!$instance) return false; + + $settingMap = $this->installationSettingsMap($customData, $dbInfo, $instance); + return $this->doCneInstall($instance, $space, $settingMap); + } + + /** + * Create instance recorder for installation. + * + * @param object $app + * @param object $space + * @param object $thirdDomain + * @param string $name + * @param string $channel + * @param array $snippets + * @access public + * @return bool|object + */ + public function createInstance($app, $space, $thirdDomain, $name = '', $k8name = '', $channel = 'stable', $snippets = array()) + { + if(empty($k8name)) $k8name = "{$app->chart}-{$this->app->user->account}-" . date('YmdHis'); //name rule: chartName-userAccount-YmdHis; + + $instanceData = new stdclass; + $instanceData->appId = $app->id; + $instanceData->appName = $app->alias; + $instanceData->name = !empty($name) ? $name : $app->alias; + $instanceData->domain = !empty($thirdDomain) ? $this->fullDomain($thirdDomain) : ''; + $instanceData->logo = $app->logo; + $instanceData->desc = $app->desc; + $instanceData->introduction = isset($app->introduction) ? $app->introduction : $app->desc; + $instanceData->source = 'cloud'; + $instanceData->channel = $channel; + $instanceData->chart = $app->chart; + $instanceData->appVersion = $app->app_version; + $instanceData->version = $app->version; + $instanceData->space = $space->id; + $instanceData->k8name = $k8name; + $instanceData->status = 'creating'; + $instanceData->createdBy = $this->app->user->account; + $instanceData->createdAt = date('Y-m-d H:i:s'); + + foreach($snippets as $fieldName => $snippetName) $instanceData->$fieldName = $snippetName; + + $this->dao->insert(TABLE_INSTANCE)->data($instanceData)->autoCheck()->exec(); + if(dao::isError()) return false; + + $instance = $this->getByID($this->dao->lastInsertID()); + return $instance; + } + + /** + * Create app instance on CNE platform. + * + * @param object $instance + * @param object $space + * @param object $settingsMap + * @param array $snippets + * @param object $app + * @access private + * @return object|bool + */ + private function doCneInstall($instance, $space, $settingsMap, $snippets = array(), $settings = array()) + { + $apiParams = new stdclass; + $apiParams->userame = $instance->createdBy; + $apiParams->cluser = ''; + $apiParams->namespace = $space->k8space; + $apiParams->name = $instance->k8name; + $apiParams->chart = $instance->chart; + $apiParams->version = $instance->version; + $apiParams->channel = $instance->channel; + $apiParams->settings_map = $settingsMap; + $apiParams->settings = $settings; + $apiParams->settings_snippets = array_values($snippets); + + if(strtolower($this->config->CNE->app->domain) == 'demo.haogs.cn') $apiParams->settings_snippets = array('quickon_saas'); // Only for demo enviroment. + + $result = $this->cne->installApp($apiParams); + if($result->code != 200) + { + $this->dao->delete()->from(TABLE_INSTANCE)->where('id')->eq($instance->id)->exec(); + return false; + } + + $this->action->create('instance', $instance->id, 'install', '', json_encode(array('result' => $result, 'apiParams' => $apiParams))); + + $instance->status = 'initializing'; + $instance->dbSettings = json_encode($apiParams->settings_map); + + $this->updateByID($instance->id, array('status' => $instance->status, 'dbSettings' => $instance->dbSettings)); + + return $instance; + } + + /* + * Uninstall app instance. + * + * @param object $instance + * @access public + * @return bool + */ + public function uninstall($instance) + { + if($instance->source == 'external') + { + $this->dao->update(TABLE_INSTANCE)->set('deleted')->eq(1)->where('id')->eq($instance->id)->exec(); + return true; + } + + $apiParams = new stdclass; + $apiParams->cluster = '';// Multiple cluster should set this field. + $apiParams->name = $instance->k8name; + $apiParams->channel = $instance->channel; + $apiParams->namespace = $instance->spaceData->k8space; + + $result = $this->cne->uninstallApp($apiParams); + $success = $result->code == 200 || $result->code == 404; + if($success) $this->dao->update(TABLE_INSTANCE)->set('deleted')->eq(1)->where('id')->eq($instance->id)->exec(); + + return $success; + } + + /* + * Start app instance. + * + * @param object $instance + * @access public + * @return object + */ + public function start($instance) + { + $apiParams = new stdclass; + $apiParams->cluster = ''; + $apiParams->name = $instance->k8name; + $apiParams->chart = $instance->chart; + $apiParams->namespace = $instance->spaceData->k8space; + $apiParams->channel = $instance->channel; + + $result = $this->cne->startApp($apiParams); + if($result->code == 200) $this->dao->update(TABLE_INSTANCE)->set('status')->eq('starting')->where('id')->eq($instance->id)->exec(); + + return $result; + } + + /* + * Stop app instance. + * + * @param object $instance + * @access public + * @return object + */ + public function stop($instance) + { + $apiParams = new stdclass; + $apiParams->cluster = '';// Mulit cluster should set this field. + $apiParams->name = $instance->k8name; + $apiParams->chart = $instance->chart; + $apiParams->namespace = $instance->spaceData->k8space; + $apiParams->channel = $instance->channel; + + $result = $this->cne->stopApp($apiParams); + if($result->code == 200) $this->dao->update(TABLE_INSTANCE)->set('status')->eq('stopping')->where('id')->eq($instance->id)->exec(); + + return $result; + } + + /** + * Upgrade app instance to higher version. + * + * @param object $instance + * @param string $toVersion + * @param string $appVersion + * @access public + * @return bool + */ + public function upgrade($instance, $toVersion, $appVersion) + { + $success = $this->cne->upgradeToVersion($instance, $toVersion); + if(!$success) return false; + + $instanceData = new stdclass; + $instanceData->version = $toVersion; + $instanceData->appVersion = $appVersion; + $this->updateByID($instance->id, $instanceData); + + return true; + } + + /** + * Upgrade to senior App. + * + * @param object $instance + * @param object $seniorApp + * @access public + * @return bool + */ + public function upgradeToSenior($instance, $seniorApp) + { + $instance->chart = $seniorApp->chart; + $instance->version = $seniorApp->version; + + $settings = new stdclass; + $settings->settings_map = new stdclass; + $settings->settings_map->nameOverride = $this->parseK8Name($instance->k8name)->chart; + + $settings->settings_map->global = new stdclass; + $settings->settings_map->global->stopped = false; + + if(!$this->cne->updateConfig($instance, $settings)) + { + dao::$errors[] = $this->lang->instance->errors->failToSenior; + return false; + } + + $seniorData = new stdclass; + $seniorData->chart = $seniorApp->chart; + $seniorData->version = $seniorApp->version; + $seniorData->appVersion = $seniorApp->app_version; + $seniorData->appID = $seniorApp->id; + $seniorData->appName = $seniorApp->alias; + $seniorData->name = $instance->name . "($seniorApp->alias)"; + + $this->dao->update(TABLE_INSTANCE)->data($seniorData)->where('id')->eq($instance->id)->exec(); + + $logExtra = new stdclass; + $logExtra->result = 'success'; + $logExtra->data = new stdclass; + $logExtra->data->oldAppName = $instance->appName; + $logExtra->data->newAppName = $seniorApp->alias; + + $this->action->create('instance', $instance->id, 'tosenior', '', json_encode($logExtra)); + + return true; + } + + /* + * Query and update instances status. + * + * @param array $instances + * @access public + * @return array new status list [{id:xx, status: xx, changed: true/false}] + */ + public function batchFresh(&$instances) + { + $statusList = $this->cne->batchQueryStatus($instances); + + $newStatusList = array(); + + foreach($instances as $instance) + { + $statusData = zget($statusList, $instance->k8name, ''); + if($statusData) + { + if($instance->status != $statusData->status || $instance->version != $statusData->version) + { + $this->dao->update(TABLE_INSTANCE) + ->set('status')->eq($statusData->status) + ->beginIF($statusData->version)->set('version')->eq($statusData->version)->fi() + ->where('id')->eq($instance->id) + ->autoCheck() + ->exec(); + $instance->status = $statusData->status; + } + } + + $status = new stdclass; + $status->id = $instance->id; + $status->status = $instance->status; + + $newStatusList[] = $status; + } + + return $newStatusList; + } + + /* + * Query and update instance status. + * + * @param object $instance + * @access public + * @return object + */ + public function freshStatus($instance) + { + $instance->runDuration = 0; + $statusResponse = $this->cne->queryStatus($instance); + if($statusResponse->code != 200) return $instance; + + $statusData = $statusResponse->data; + $instance->runDuration = intval($statusData->age); // Run duration used in view page. + + if($instance->status != $statusData->status || $instance->version != $statusData->version) + { + $instance->status = $statusData->status; + + $this->dao->update(TABLE_INSTANCE) + ->set('status')->eq($statusData->status) + ->beginIF($statusData->version)->set('version')->eq($statusData->version)->fi() + ->where('id')->eq($instance->id) + ->autoCheck() + ->exec(); + } + + return $instance; + } + + /** + * Save auto backup settings. + * + * @param object $instance + * @access public + * @return bool + */ + public function saveAutoBackupSettings($instance) + { + /* Cycle days is always is 1 at present. */ + $settings = fixer::input('post') + ->setDefault('backupTime', '1:00') + ->setDefault('cycleDays', '1') + ->setDefault('keepDays', '10') + ->setDefault('autoBackup', array()) + ->get(); + $settings->autoBackup = in_array('true', $settings->autoBackup); + + if(!preg_match("/^([0-2][0-9]):([0-5][0-9])$/", $settings->backupTime, $parts)) + { + dao::$errors[] = $this->lang->instance->backup->invalidTime; + return false; + } + + /* Save cron task. */ + list($hour, $minute) = explode(':', $settings->backupTime); + $cronData = new stdclass; + $cronData->m = intval($minute); + $cronData->h = intval($hour); + //$cronData['dom'] = intval($settings->cycleDays); + $cronData->dom = '*'; + $cronData->mon = '*'; + $cronData->dow = '*'; + + $cron = $this->dao->select('*')->from(TABLE_CRON)->where('objectID')->eq($instance->id)->fetch(); + if($cron) + { + $this->dao->update(TABLE_CRON)->autoCheck()->data($cronData)->where('id')->eq($cron->id)->exec(); + } + else + { + $cronData->objectID = $instance->id; + $this->dao->insert(TABLE_CRON)->data($cronData)->exec(); + } + if($this->dao->isError()) return false; + + /* Save instance backup settings. */ + $this->dao->update(TABLE_INSTANCE)->set('autoBackup')->eq($settings->autoBackup)->set('backupKeepDays')->eq($settings->keepDays)->where('id')->eq($instance->id)->exec(); + if($this->dao->isError()) return false; + + $this->action->create('instance', $instance->id, 'saveAutoBackupSettings', '', json_encode(array('data' => $settings))); + return true; + } + + /** + * Get auto backup settings. + * + * @param int $instnaceID + * @access public + * @return object + */ + public function getAutoBackupSettings($instanceID) + { + $instance = $this->getByID($instanceID); + + $cron = $this->dao->select('*')->from(TABLE_CRON)->where('objectID')->eq($instanceID)->limit(1)->fetch(); + if(!$cron) $cron = new stdclass; + + $hour = substr('0' . zget($cron, 'h', '1'), -2, 2); + $minute = substr('0' . zget($cron, 'm', '00'), -2, 2); + + $settings = new stdclass; + $settings->backupTime = "{$hour}:{$minute}"; + $settings->autoBackup = boolval(zget($instance, 'autoBackup', false)); + $settings->keepDays = $settings->autoBackup ? intval(zget($instance, 'backupKeepDays', 7)) : 7; // Set default value to 7 days. + $settings->cycleDays = 1; // Cycle days is always is 1 at present. + + return $settings; + } + + /** + * Save auto backup settings. + * + * @param object $instance + * @access public + * @return bool + */ + public function saveAutoRestoreSettings($instance) + { + /* Cycle days is always is 1 at present. */ + $settings = fixer::input('post') + ->setDefault('restoreTime', '1:00') + ->setDefault('cycleDays', '1') + ->setDefault('autoRestore', array()) + ->get(); + $settings->autoRestore = in_array('true', $settings->autoRestore); + + if(!preg_match("/^([0-2][0-9]):([0-5][0-9])$/", $settings->restoreTime, $parts)) + { + dao::$errors[] = $this->lang->instance->restore->invalidTime; + return false; + } + + /* Save cron task. */ + list($hour, $minute) = explode(':', $settings->restoreTime); + $cronData = new stdclass; + $cronData->m = intval($minute); + $cronData->h = intval($hour); + //$cronData['dom'] = intval($settings->cycleDays); + $cronData->dom = '*'; + $cronData->mon = '*'; + $cronData->dow = '*'; + + $cron = $this->dao->select('*')->from(TABLE_CRON)->where('objectID')->eq($instance->id)->fetch(); + if($cron) + { + $this->dao->update(TABLE_CRON)->autoCheck()->data($cronData)->where('id')->eq($cron->id)->exec(); + } + else + { + $cronData->objectID = $instance->id; + $this->dao->insert(TABLE_CRON)->data($cronData)->exec(); + } + if($this->dao->isError()) return false; + + /* Save instance restore settings. */ + $this->dao->update(TABLE_INSTANCE)->set('autoRestore')->eq($settings->autoRestore)->where('id')->eq($instance->id)->exec(); + if($this->dao->isError()) return false; + + $this->action->create('instance', $instance->id, 'saveAutoRestoreSettings', '', json_encode(array('data' => $settings))); + return true; + } + + /** + * Get auto restore settings. + * + * @param int $instnaceID + * @access public + * @return object + */ + public function getAutoRestoreSettings($instanceID) + { + $instance = $this->getByID($instanceID); + + $cron = $this->dao->select('*')->from(TABLE_CRON)->where('objectID')->eq($instanceID)->limit(1)->fetch(); + if(!$cron) $cron = new stdclass; + + $hour = substr('0' . zget($cron, 'h', '1'), -2, 2); + $minute = substr('0' . zget($cron, 'm', '00'), -2, 2); + + $settings = new stdclass; + $settings->restoreTime = "{$hour}:{$minute}"; + $settings->autoRestore = boolval(zget($instance, 'autoRestore', false)); + $settings->cycleDays = 1; // Cycle days is always is 1 at present. + + return $settings; + } + + /** + * Do auto backup cron task. + * + * @access public + * @return void + */ + public function autoBackup() + { + $instanceList = $this->dao->select('*')->from(TABLE_INSTANCE) + ->where('deleted')->eq(0) + ->andWhere('autoBackup')->eq(true) + ->andWhere('status')->eq('running') + ->fetchAll('id'); + + /* Load all crons that instance is enable auto backup. */ + $crons = $this->dao->select('*')->from(TABLE_CRON)->where('objectID')->in(array_keys($instanceList))->fetchAll(); + + $nowHour = intval(date('H')); + $nowMiute = intval(date('i')); + foreach($crons as $cron) + { + $hour = intval($cron->h); + $minute = intval($cron->m); + if(!($nowHour == $hour and $nowMiute == $minute)) continue; + + /* 1. create new backup of instance. */ + $system = new stdclass; + $system->account = 'auto'; // The string 'auto' should be kept for System. + + $instance = zget($instanceList, $cron->objectID); + $instance->spaceData = $this->dao->select('*')->from(TABLE_SPACE)->where('id')->eq($instance->space)->fetch(); + $this->backup($instance, $system); + + $this->action->create('instance', $instance->id, 'autoBackup'); + + /* 2. Pick latest successful backup recorder. */ + $latestBackup = null; + $backupList = $this->backupList($instance); + foreach($backupList as $backup) + { + if(empty($latestBackup) or $backup->status == 'completed' && $backup->create_time > $latestBackup->create_time) + { + $latestBackup = $backup; + } + } + + /* 3. delete expired backup. Get backup list of instance, then check every backup is expired or not.*/ + foreach($backupList as $backup) + { + if($backup->creator != 'auto') continue; // Only delete data madde by auto backup. + if($latestBackup && $latestBackup->name == $backup->name) continue; // Keep latest successful backup. + + $deadline = intval($backup->create_time) + $instance->backupKeepDays * 24 * 3600; + //$deadline = intval($backup->create_time) + 300; // Debug codes: delete backup if it's life is older 5 minuts. + if($deadline < time()) + { + $this->deleteBackup($instance, $backup->name); + $this->action->create('instance', $instance->id, 'deleteExpiredBackup', '', json_encode(array('data' => $backup))); + } + } + } + } + + /** + * Delete backup. + * + * @param object $instance + * @param string $backupName + * @access public + * @return bool + */ + public function deleteBackup($instance, $backupName) + { + return $this->cne->deleteBackup($instance, $backupName); + } + + /** + * Backup instance. + * + * @param object $instance + * @param object $user + * @access public + * @return bool + */ + public function backup($instance, $user) + { + $result = $this->cne->backup($instance, $user->account); + if($result->code != 200) return false; + + return true; + } + + /** + * Restore instance. + * + * @param object $instance + * @param object $user + * @param string $backupName + * @access public + * @return bool + */ + public function restore($instance, $user, $backupName) + { + $result = $this->cne->restore($instance, $backupName, $user->account); + if($result->code != 200) return false; + + return true; + } + + /* Run restore latest successful manual backup recorder automatically. + * + * @access public + * @return void + */ + public function autoRestore() + { + $instanceList = $this->dao->select('*')->from(TABLE_INSTANCE) + ->where('deleted')->eq(0) + ->andWhere('autoRestore')->eq(true) + ->andWhere('status')->eq('running') + ->fetchAll('id'); + + /* Load all crons that instance is enable auto restore. */ + $crons = $this->dao->select('*')->from(TABLE_CRON)->where('objectID')->in(array_keys($instanceList))->fetchAll(); + + $nowHour = intval(date('H')); + $nowMiute = intval(date('i')); + foreach($crons as $cron) + { + $hour = intval($cron->h); + $minute = intval($cron->m); + if(!($nowHour == $hour and $nowMiute == $minute)) continue; + + /* Pick latest successful manual backup recorder. */ + $latestBackup = null; + $instance = $this->getByID($cron->objectID); + $backupList = $this->backupList($instance); + foreach($backupList as $backup) + { + if($backup->username == 'auto') continue; // Only restore manual backup. + if(empty($latestBackup) or $backup->status == 'completed' && $backup->create_time > $latestBackup->create_time) + { + $latestBackup = $backup; + } + } + + if($latestBackup) + { + $sysUser = new stdclass; + $sysUser->account = 'auto'; + $this->restore($instance, $sysUser, $latestBackup->name); + + $this->action->create('instance', $instance->id, 'autoRestore'); + } + } + } + + /** + * Get backup list of instance. + * + * @param object $instance + * @access public + * @return array + */ + public function backupList($instance) + { + $result = $this->cne->backupList($instance); + if(empty($result) || $result->code != 200 || empty($result->data)) return array(); + + $backupList = $result->data; + usort($backupList, function($backup1, $backup2){ return $backup1->create_time < $backup2->create_time; }); + + $accounts = array_column($backupList, 'creator'); + foreach($backupList as $backup) $accounts = array_merge($accounts, array_column($backup->restores, 'creator')); + + $accounts = array_unique($accounts); + + $users = $this->dao->select('account,realname')->from(TABLE_USER)->where('account')->in($accounts)->fetchPairs('account', 'realname'); + + foreach($backupList as &$backup) + { + $backup->latest_restore_time = 0; + $backup->latest_restore_status = ''; + $backup->username = zget($users, $backup->creator); + /* Mount backup operator info and latest restore info. */ + foreach($backup->restores as &$restore) + { + $restore->username = zget($users, $restore->creator); + + if($restore->create_time > $backup->latest_restore_time) + { + $backup->latest_restore_time = $restore->create_time; + $backup->latest_restore_status = $restore->status; + } + } + } + + return $backupList; + } + + /** + * Mount DB name and alias to array for select options. + * + * @param array $databases + * @access public + * @return array + */ + public function dbListToOptions($databases) + { + $dbList = array(); + foreach($databases as $database) $dbList[$database->name] = zget($database, 'alias', $database->name); + + return $dbList; + } + + /** + * Restore instance by data from k8s cluster. + * + * @access public + * @return void + */ + public function restoreInstanceList() + { + $k8AppList = $this->cne->instancelist(); + $k8NameList = array_keys($k8AppList); + + //软删除不存在的数据 + $this->dao->update(TABLE_INSTANCE)->set('deleted')->eq(1)->where('k8name')->notIn($k8NameList)->exec(); + foreach($k8AppList as $k8App) + { + $existInstance = $this->dao->select('id')->from(TABLE_INSTANCE)->where('k8name')->eq($k8App->name)->fetch(); + if($existInstance) continue; + + $this->loadModel('store'); + $marketApp = $this->store->getAppInfo(0, false, $k8App->chart, $k8App->version, $k8App->channel); + if(empty($marketApp)) continue; + + $instanceData = new stdclass; + $instanceData->appId = $marketApp->id; + $instanceData->appName = $marketApp->alias; + $instanceData->name = $marketApp->alias; + $instanceData->logo = $marketApp->logo; + $instanceData->desc = $marketApp->desc; + $instanceData->introduction = isset($marketApp->introduction) ? $marketApp->introduction : $marketApp->desc; + $instanceData->source = 'cloud'; + $instanceData->channel = $k8App->channel; + $instanceData->chart = $k8App->chart; + $instanceData->appVersion = $marketApp->app_version; + $instanceData->version = $k8App->version; + $instanceData->k8name = $k8App->name; + $instanceData->status = 'stopped'; + + $parsedK8Name = $this->parseK8Name($k8App->name); + + $instanceData->createdBy = $k8App->username ? $k8App->username : $parsedK8Name->createdBy; + $instanceData->createdAt = $parsedK8Name->createdAt; + + $space = $this->dao->select('id,k8space')->from(TABLE_SPACE)->where('k8space')->eq($k8App->namespace)->fetch(); + if(empty($space)) $space = $this->loadModel('space')->defaultSpace($instanceData->createdBy); + + $instanceData->space = $space ? $space->id : $defaultSpace->id; + + $this->dao->insert(TABLE_INSTANCE)->data($instanceData)->exec(); + } + } + + /** + * Parse K8Name to get more data: chart, created time, user name. + * + * @param string $k8Name + * @access public + * @return object + */ + public function parseK8Name($k8Name) + { + $datePosition = strripos($k8Name, '-'); + $createdAt = trim(substr($k8Name, $datePosition), '-'); + + $createdBy = trim(substr($k8Name, 0, $datePosition), '-'); + $accountPosition = strripos($createdBy, '-'); + $createdBy = trim(substr($createdBy, $accountPosition), '-'); + + $parsedData = new stdclass; + $parsedData->chart = trim(substr($k8Name, 0, $accountPosition)); + $parsedData->createdBy = trim($createdBy, '-'); + $parsedData->createdAt = date('Y-m-d H:i:s', strtotime($createdAt)); + + return $parsedData; + } + + /** + * Delete expired instances if run in demo mode. + * + * @access public + * @return void + */ + public function deleteExpiredDemoInstance() + { + $deadline = date('Y-m-d H:i:s', strtotime("-{$this->config->demoAppLife} minutes")); + $demoAccounts = array_filter(explode(',', $this->config->demoAccounts)); + + $instanceList = $this->dao->select('*')->from(TABLE_INSTANCE) + ->where('deleted')->eq(0) + ->andWhere('createdAt')->lt($deadline) + ->andWhere('createdBy')->in($demoAccounts) + ->fetchAll(); + if(empty($instanceList)) return; + + $spaceList = $this->dao->select('*')->from(TABLE_SPACE)->where('id')->in(array_column($instanceList, 'space'))->fetchAll('id'); + + foreach($instanceList as $instance) + { + $instance->spaceData = zget($spaceList, $instance->space); + if(empty($instance->spaceData)) continue; + + $this->uninstall($instance); + } + } + + /** + * Filter memory options that smaller then current memory size. + * + * @param object $resources + * @access public + * @return array + */ + public function filterMemOptions($resources) + { + $currentMemory = intval($resources->min->memory / 1024); + + $options = []; + foreach($this->lang->instance->memOptions as $size => $text) + { + if($size > $currentMemory) $options[$size] = $text; + } + + return $options; + } + + /** + * Print suggested memory size by current memory usage. Show suggested messge when memory usage more than 90%. + * + * @param object $memUsage + * @param array $memoryOptions + * @access public + * @return void + */ + public function printSuggestedMemory($memUsage, $memoryOptions) + { + if($memUsage->rate < 90) return; + + foreach($memoryOptions as $size => $memText) + { + if($size > ($memUsage->limit / 1024)) + { + printf($this->lang->instance->adjustMemorySize, $memText); + return ; + } + } + } + + /** + * Print instance status. + * + * @param object $instance + * @param bool $showText + * @access public + * @return void + */ + public function printStatus($instance, $showText = true) + { + $html = zget($this->lang->instance->htmlStatuses, $instance->status, $this->lang->instance->htmlStatuses['busy']); + $statusText = zget($this->lang->instance->statusList, $instance->status, ''); + printf($html, $statusText); + } + + /** + * Print CPU usage. + * + * @param object $instance + * @param object $metrics + * @param string $type 'bar' is progress bar, 'pie' is progress pie. + * @static + * @access public + * @return viod + */ + public static function printCpuUsage($instance, $metrics, $type = 'bar') + { + $rate = $metrics->rate; + $tip = "{$rate}% = {$metrics->usage} / {$metrics->limit}"; + + if(strtolower($type) == 'pie') return commonModel::printProgressPie($rate, '', $tip); + + $valueType = 'percent'; + if($instance->status == 'stopped') $valueType = ''; + + return commonModel::printProgressBar($rate, '', $tip, $valueType); + } + + /** + * Print memory usage. + * + * @param object $instance + * @param object $metrics + * @param string $type 'bar' is progress bar, 'pie' is progress pie. + * @static + * @access public + * @return viod + */ + public static function printMemUsage($instnace, $metrics, $type = 'bar') + { + $rate = $metrics->rate; + $tip = "{$rate}% = " . helper::formatKB($metrics->usage / 1024) . ' / ' . helper::formatKB($metrics->limit / 1024); + + if(strtolower($type) == 'pie') return commonModel::printProgressPie($rate, '', $tip); + + $valueType = 'tip'; + if($instnace->status == 'stopped') $valueType = ''; + + return commonModel::printProgressBar($rate, '', $tip, $valueType); + } + + /* + * Print action buttons with icon. + * + * @param object $instance + * @access public + * @return void + */ + public function printIconActions($instance) + { + $actionHtml = ''; + + $disableStart = !$this->canDo('start', $instance); + $actionHtml .= html::commonButton("", "instance-id='{$instance->id}' title='{$this->lang->instance->start}'" . ($disableStart ? ' disabled ' : ''), "btn-start btn btn-lg btn-action"); + + $disableStop = !$this->canDo('stop', $instance); + $actionHtml .= html::commonButton('', "instance-id='{$instance->id}' title='{$this->lang->instance->stop}'" . ($disableStop ? ' disabled ' : ''), 'btn-stop btn btn-lg btn-action'); + + if(empty($instance->solution)) + { + $disableUninstall = !$this->canDo('uninstall', $instance); + $actionHtml .= html::commonButton('', "instance-id='{$instance->id}' title='{$this->lang->instance->uninstall}'" . ($disableUninstall ? ' disabled ' : ''), 'btn-uninstall btn btn-lg btn-action'); + } + + if($instance->domain) + { + $disableVisit = !$this->canDo('visit', $instance); + $actionHtml .= html::a($this->url($instance), '', '_blank', "title='{$this->lang->instance->visit}' class='btn btn-lg btn-action btn-link'" . ($disableVisit ? ' disabled style="pointer-events: none;"' : '')); + } + + echo $actionHtml; + } + + /* + * Print action buttons with text. + * + * @param object $instance + * @access public + * @return void + */ + public function printTextActions($instance) + { + $actionHtml = ''; + + $disableStart = !$this->canDo('start', $instance); + $actionHtml .= html::commonButton($this->lang->instance->start, "instance-id='{$instance->id}' title='{$this->lang->instance->start}'" . ($disableStart ? ' disabled ' : ''), "btn-start btn label label-outline label-primary label-lg"); + + $disableStop = !$this->canDo('stop', $instance); + $actionHtml .= html::commonButton($this->lang->instance->stop, "instance-id='{$instance->id}' title='{$this->lang->instance->stop}'" . ($disableStop ? ' disabled ' : ''), 'btn-stop btn label label-outline label-warning label-lg'); + + if(empty($instance->solution)) + { + $disableUninstall = !$this->canDo('uninstall', $instance); + $actionHtml .= html::commonButton($this->lang->instance->uninstall, "instance-id='{$instance->id}' title='{$this->lang->instance->uninstall}'" . ($disableUninstall ? ' disabled ' : ''), 'btn-uninstall btn label label-outline label-danger label-lg'); + } + + if($instance->domain) + { + $disableVisit = !$this->canDo('visit', $instance); + $actionHtml .= html::a($this->url($instance), $this->lang->instance->visit, '_blank', "title='{$this->lang->instance->visit}' class='btn btn-primary label-lg'" . ($disableVisit ? ' disabled style="pointer-events: none;"' : '')); + } + + echo $actionHtml; + } + + /** + * Print backup button of instance. + * + * @param object $instance + * @access public + * @return void + */ + public function printBackupBtn($instance) + { + $disabled = $instance->status == 'running' ? '' : 'disabled'; + $title = empty($disabled) ? $this->lang->instance->backup->create : $this->lang->instance->backupOnlyRunning; + $btn = html::commonButton($this->lang->instance->backup->create, "instance-id='{$instance->id}' title='{$title}' {$disabled}", "btn-backup btn btn-primary"); + + echo $btn; + } + + /** + * Print restore button of instance. + * + * @param object $instance + * @param object $backup + * @access public + * @return void + */ + public function printRestoreBtn($instance, $backup) + { + $disabled = $instance->status == 'running' && strtolower($backup->status) == 'completed' ? '' : 'disabled'; + $title = empty($disabled) ? $this->lang->instance->backup->restore : $this->lang->instance->restoreOnlyRunning; + $btn = html::commonButton($this->lang->instance->backup->restore, "instance-id='{$instance->id}' title='{$title}' {$disabled} backup-name='{$backup->name}'", "btn-restore btn btn-info"); + + echo $btn; + } + + /** + * Print button for managing database. + * + * @param object $db + * @param object $instance + * @access public + * @return void + */ + public function printDBAction($db, $instance) + { + $disabled = $db->ready ? '' : 'disabled'; + $btnHtml = html::commonButton($this->lang->instance->management, "{$disabled} data-db-name='{$db->name}' data-db-type='{$db->db_type}' data-id='{$instance->id}'", 'db-login btn btn-primary'); + + echo $btnHtml; + } + + /** + * Print message of action log of instance. + * + * @param object $instance + * @param object $log + * @access public + * @return void + */ + public function printLog($instance, $log) + { + $action = zget($this->lang->instance->actionList, $log->action, $this->lang->actions); + + $logText = $log->actorName . ' ' . sprintf($action, $instance->name, $log->comment); + + $extra = json_decode($log->extra); + if(empty($extra) or !isset($extra->data)) + { + echo $logText; + return; + } + + switch($log->action) + { + case 'editname': + $oldName = zget($extra->data, 'oldName', ''); + $newName = zget($extra->data, 'newName', ''); + $logText .= ', ' . sprintf($this->lang->instance->nameChangeTo, $oldName, $newName); + break; + case 'upgrade': + $oldVersion = zget($extra->data, 'oldVersion', ''); + $newVersion = zget($extra->data, 'newVersion', ''); + $logText .= ', ' . sprintf($this->lang->instance->versionChangeTo, $oldVersion, $newVersion); + break; + case 'tosenior': + $oldAppName = zget($extra->data, 'oldAppName', ''); + $newAppName = zget($extra->data, 'newAppName', ''); + $logText .= ', ' . sprintf($this->lang->instance->toSeniorSerial, $oldAppName, $newAppName); + break; + case 'saveautobackupsettings': + $enableAutoBackup = zget($extra->data, 'autoBackup', 0); + $logText .= ': ' . ($enableAutoBackup ? $this->lang->instance->enableAutoBackup : $this->lang->instance->disableAutoBackup); + break; + } + + echo $logText; + } + + /* + * Convert CPU digital to readable format. + * + * @param array $cpuList + * @access public + * @return array + */ + public function getCpuOptions($cpuList) + { + $newList = array(); + foreach($cpuList as $cpuValue) $newList[$cpuValue] = $cpuValue . $this->lang->instance->cpuCore; + return $newList; + } + + /* + * Convert memory digital to readable format. + * + * @param array $memList + * @access public + * @return array + */ + public function getMemOptions($memList) + { + $newList = array(); + foreach($memList as $memValue) $newList[$memValue] = helper::formatKB(intval($memValue / 1024)); + return $newList; + } + + /* + * Get instance switcher. + * + * @param object $instance + * @access public + * @return string + */ + public function getSwitcher($instance) + { + $instanceLink = helper::createLink('instance', 'view', "id=$instance->id"); + + $output = "
"; + $output .= html::a($instanceLink, $instance->appName, '', 'class="btn"'); + $output .= "
"; + + return $output; + } + + /** + * Get switcher of custom installation page of store. + * + * @param object $app + * @access public + * @return array + */ + public function getInstallSwitcher($app) + { + $output = $this->loadModel('store')->getAppViewSwitcher($app); + $output .= "
"; + $output .= html::a(helper::createLink('instance', 'install', "id=$app->id"), $this->lang->instance->installApp, '', 'class="btn"'); + $output .= "
"; + + return $output; + } + + /** + * Get switcher of custom installation page of store. + * + * @param object $app + * @access public + * @return array + */ + public function getCustomInstallSwitcher($app) + { + $output = $this->loadModel('store')->getAppViewSwitcher($app); + $output .= "
"; + $output .= html::a(helper::createLink('instance', 'custominstall', "id=$app->id"), $this->lang->instance->customInstall, '', 'class="btn"'); + $output .= "
"; + + return $output; + } + + /** + * Get senior app list. The instance can be switched to senior App. + * + * @param object $instance + * @param string $channel + * @access public + * @return array + */ + public function seniorAppList($instance, $channel) + { + $appList = array(); + foreach(zget($this->config->instance->seniorChartList, $instance->chart, array()) as $chart) + { + $cloudApp = $this->loadModel('store')->getAppInfoByChart($chart, $channel, false); + if($cloudApp) $appList[] = $cloudApp; + } + + return $appList; + } + + /** + * Check the free memory of cluster is enough to install app. + * + * @param object $cloudApp + * @access public + * @return bool + */ + public function enoughMemory($cloudApp) + { + $clusterResource = $this->cne->cneMetrics(); + $freeMemory = intval($clusterResource->metrics->memory->allocatable * 0.9); // Remain 10% memory for system. + + return $freeMemory >= $cloudApp->memory; + } +} diff --git a/module/instance/view/advance.html.php b/module/instance/view/advance.html.php new file mode 100644 index 0000000000..f7b3c2d37d --- /dev/null +++ b/module/instance/view/advance.html.php @@ -0,0 +1,192 @@ +
+ + +
+
instance->visitIP;?>
+
+
+ + $value):?> + + + + + +
+
+ +
+
instance->mem;?>
+
+
+
+ + + + + + + + + + + + + + + + + +
+ instance->currentMemory;?>:resources->memory / 1024);?> + instance->printSuggestedMemory($instanceMetric->memory, $lang->instance->memOptions);?> +
instance->adjustMem;?>instance->filterMemOptions($currentResource), '', "class='form-control'");?>
+ instance->saveSetting, "id='memBtn' instance-id='$instance->id'", 'btn btn-primary'); ?> +
+
+
+ features->ldap)):?> +
+
system->ldapManagement;?>
+
+
+
+ + + system->hasSystemLDAP();?> + ldapSnippetName ? 'true' : '' ;?> + + + + + + + + + + +
$lang->instance->enableLDAP), $enableLDAP, ($LDAPInstalled ? '' : 'disabled'));?> + + instance->systemLDAPInactive;?> + instance->toSystemLDAP, '', "class='btn btn-default'");?> + +
+ instance->saveSetting, "id='ldapBtn' instance-id='$instance->id'" . ($LDAPInstalled ? '' : 'disabled'), 'btn btn-primary');?> +
+
+
+ + features->mail)):?> +
+
system->SMTP->common;?>
+
+
+
+ + + system->isSMTPEnabled();?> + smtpSnippetName ? 'true' : '' ;?> + + + + + + + + + + +
$lang->instance->enableSMTP), $enableSMTP, ($SMTPInstalled ? '' : 'disabled'));?> + + instance->systemSMTPInactive;?> + instance->toSystemSMTP, '', "class='btn btn-default'");?> + +
+ instance->saveSetting, "id='smtpBtn' instance-id='$instance->id'" . ($SMTPInstalled ? '' : 'disabled'), 'btn btn-primary');?> +
+
+
+ + + scalable) && $config->edition == 'biz'):?> +
+
instance->scalable;?>
+
+
+
+ + + + + + + +
instance->componentFields['replicas'];?> + replicas;?> + 1, 2=>2, 3=>3, 4=>4), intval($currentResource->replicas) ? : 1, " id='replicas-input' max='4' min='1' class='form-control scalable-input hide'");?> + + instance->change, 'id="replicas-edit"', 'btn btn-primary ' . (in_array($instance->status, array('uninstalling', 'destroying', 'destroyed', 'unknown', 'abnormal')) ? 'disabled' : ''));?> + instance->saveSetting, "id='replicas-save' instance-id='$instance->id'", 'btn btn-primary hide');?> +
+
+
+ + + +
+
instance->customSetting;?>
+
+
+
+ + + + + + + + + + + + + + + +
label;?> + name, $item->default, "class='form-control' placeholder='{$item->label}'");?> +
+ instance->saveSetting, "id='customBtn' instance-id='$instance->id'", 'btn btn-primary'); ?> +
+
+
+ + +
+
+
instance->dbList;?>
+
+
+ + + + + + + + + + + + + + + + + + + +
instance->dbName;?>instance->dbType;?>instance->dbStatus;?>instance->action;?>
db_name;?>db_type;?>ready ? $lang->instance->dbReady : $lang->instance->dbWaiting;?>instance->printDBAction($db, $instance);?>
+
+ +
diff --git a/module/instance/view/backup.html.php b/module/instance/view/backup.html.php new file mode 100644 index 0000000000..9e16c19fdf --- /dev/null +++ b/module/instance/view/backup.html.php @@ -0,0 +1,80 @@ +
+
+ instance->backup->keepBackupBySystem;?> +
+ refresh, '', "class='btn'");?> + id}", '', true), $lang->instance->backup->autoBackup, '', "class='iframe btn btn-primary' title='{$lang->instance->backup->autoBackup}' data-width='520' data-app='space'");?> + id}", '', true), $lang->instance->restore->autoRestore, '', "class='iframe btn btn-primary' title='{$lang->instance->restore->autoRestore}' data-width='520' data-app='space'");?> + config->instance->enableAutoRestore ? $autoRestoreBtn : $autoBackupBtn;?> + instance->printBackupBtn($instance);?> +
+
+ +
+

+ instance->errors->noBackup;?> + instance->printBackupBtn($instance);?> +

+
+ + + + + + + + + + + + + + + + + + + + + + backup_details->db[0]) ? intval(zget($backup->backup_details->db[0], 'size', 0)) : 0;?> + backup_details->volume[0]) ? intval(zget($backup->backup_details->volume[0], 'doneBytes', 0)) : 0;?> + + + + + + + + + +
instance->backup->date;?>instance->backup->operator;?>instance->backup->dbStatus;?>instance->backup->dbSize;?>instance->backup->restoreDate;?>instance->backup->dbStatus;?> + instance->backup->action;?> +
create_time);?>'>create_time);?>instance->backup->operators, $backup->username, $backup->username);?>instance->backup->statusList, strtolower($backup->status));?> + latest_restore_time) : '';?>'> + latest_restore_time ? date('Y-m-d', $backup->latest_restore_time) : '';?> + + latest_restore_status ? zget($lang->instance->restore->statusList, $backup->latest_restore_status, '') : '';?>instance->printRestoreBtn($instance, $backup);?>
+ + +
diff --git a/module/instance/view/backupsettings.html.php b/module/instance/view/backupsettings.html.php new file mode 100644 index 0000000000..674aa4ed2d --- /dev/null +++ b/module/instance/view/backupsettings.html.php @@ -0,0 +1,59 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.cn + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +app->getModuleRoot() . '/common/view/datepicker.html.php';?> +instance->notices);?> +id);?> +
+
+
+

instance->backup->autoBackup;?>

+
+
+
+

$lang->instance->backup->enableAutoBackup), $backupSettings->autoBackup ? 'true' : '');?>

+ + + + + + + + + + + + + + + + + + +
instance->backup->backupTime;?> +
+
+ backupTime, "class='form-control form-time' data-picker-position='bottom-right' maxlength='20'");?> +
+
+
instance->backup->cycleDays;?> +
instance->backup->cycleList, $backupSettings->cycleDays, "class='form-control' maxlength='20'");?>
+
instance->backup->keepDays;?> +
keepDays, "class='form-control' maxlength='2' min='1' max='30'");?>
+ instance->backup->keepDayRange;?> +
+
save, "id='saveSetting'", 'btn btn-primary');?>
+
+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/instance/view/baseinfo.html.php b/module/instance/view/baseinfo.html.php new file mode 100644 index 0000000000..16f2912585 --- /dev/null +++ b/module/instance/view/baseinfo.html.php @@ -0,0 +1,115 @@ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + status == 'running'):?> + + + + + + + + + + + + + + + + +
instance->status;?> + instance->printStatus($instance);?> +
instance->source;?> + instance->sourceList, $instance->source, '');?> +
instance->appTemplate;?>createLink('store', 'appView', "id=$instance->appID"), $instance->appName);?>
instance->installAt;?>createdAt, 0, 16);?>
instance->runDuration;?>runDuration);?>
instance->defaultAccount;?>username;?>
instance->defaultPassword;?>password;?>
+
+
+
+
instance->cpuUsage;?>
+ instance->printCpuUsage($instance, $instanceMetric->cpu, 'pie');?> +
+
+
instance->memUsage;?>
+ instance->printMemUsage($instance, $instanceMetric->memory, 'pie');?> +
+
+
+
+
+
+solution) && array_key_exists(strtolower($instance->chart), $this->config->instance->seniorChartList)):?> +
+
+
instance->upgradeToSenior;?>
+
+
+

+ + $seniorApp):?> + 0) $seniorAppHtml .= $lang->instance->or;?> + id"), $seniorApp->alias, '_blank');?> + + appID"), $instance->appName, '_blank');?> + instance->descOfSwitchSerial, $currentAppUrl, $seniorAppHtml);?> + instance->serialDiff, '_blank', "id='serialDiff' class=''");?> +

+
+ status == 'stopped' ? '' : 'disabled';?> + + inLink('toSenior', "id={$instance->id}&seniorAppID={$seniorApp->id}", '', true);?> + instance->switchTo . $seniorApp->alias, '', $disabled . " class='iframe btn btn-primary' title='{$lang->instance->upgrade}' data-width='520' data-app='space'");?> + + + instance->stopInstanceTips;?> + +
+
+
+ +
+
+
instance->operationLog;?>
+
+
+ + + + + + + + + + + + + + + + + +
instance->log->date;?>instance->log->message;?>
date;?>instance->printLog($instance, $log);?>
+ +
+
diff --git a/module/instance/view/custominstall.html.php b/module/instance/view/custominstall.html.php new file mode 100644 index 0000000000..6571490d43 --- /dev/null +++ b/module/instance/view/custominstall.html.php @@ -0,0 +1,69 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.com + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> + +
+
+

instance->customInstall;?>

+ +
+

createLink('store', 'appView', "id={$appID}"), $lang->instance->noComponent . ' ' . $lang->goback, '', "class='btn btn-info'");?>

+
+ + +
+ +
+ + + settings as $setting):?> + type == 'choice') + { + $selectOptions = array_combine($setting->options, $setting->options); + if(strpos($setting->field, 'resources_cpu') !== false) $selectOptions = $this->instance->getCpuOptions($setting->options); + if(strpos($setting->field, 'resources_memory') !== false) $selectOptions = $this->instance->getMemOptions($setting->options); + } + ?> + + + + + + + +
lang->instance->componentFields, $setting->name, $setting->name);?> +
+ type == 'choice') echo html::select($setting->field, $selectOptions, $setting->value, "class='form-control chosen'");?> + type == 'int') echo html::number($setting->field, $setting->value, "class='form-control'", "require");?> +
+
+
+ + +
instance->install);?>
+ +
+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/instance/view/editname.html.php b/module/instance/view/editname.html.php new file mode 100644 index 0000000000..63fe7dde60 --- /dev/null +++ b/module/instance/view/editname.html.php @@ -0,0 +1,28 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.com + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +
+
+

instance->editName;?>

+
+
+ + + + + +
instance->name;?>name, "class='form-control' maxlength='50'");?>
+
+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/instance/view/install.html.php b/module/instance/view/install.html.php new file mode 100644 index 0000000000..f187c398cf --- /dev/null +++ b/module/instance/view/install.html.php @@ -0,0 +1,118 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.cn + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +instance->notices);?> +
+
+
+

instance->installApp . $cloudApp->alias . $cloudApp->app_version;?>

+
+
' class="cell not-watch load-indicator main-form"> + + + + + + + + + + + + + +
instance->name;?> +
+ alias, "class='form-control' maxlength='20'");?> +
+
instance->domain;?> +
+ + cne->sysDomain();?> +
+
+
instance->advanceOption . "", '', "data-toggle='collapse'");?>
+ + + + + + + + + + features->ldap) && $this->loadModel('system')->hasSystemLDAP()):?> + + + + + + + features->mail) && $this->loadModel('system')->isSMTPEnabled()):?> + + + + + + + dependencies->mysql) and $mysqlList or isset($cloudApp->dependencies->postgresql) and $pgList):?> + + + + + + + dependencies->mysql) and $mysqlList):?> + + + + + + dependencies->postgresql) and $pgList):?> + + + + + + + +
instance->version;?> +
+ +
+
+
+ system->ldapSnippetName() => $lang->instance->enableLDAP));?> +
+
+
+ system->smtpSnippetName() => $lang->instance->enableSMTP));?> +
+
instance->dbType;?> +
+ instance->dbTypes, 'sharedDB');?> +
+
instance->howToSelectDB, '_blank');?>
+
+ +
+
+
+ +
+
+
instance->install);?>
+
+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/instance/view/resourceerror.html.php b/module/instance/view/resourceerror.html.php new file mode 100644 index 0000000000..42334cf10d --- /dev/null +++ b/module/instance/view/resourceerror.html.php @@ -0,0 +1,22 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.cn + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +
+
+
+

instance->errors->notEnoughResource;?>

+
+

instance->errors->notEnoughMemory, $cloudApp->alias, $requiredMemory, $freeMemory, $gapMemory);?>

+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/instance/view/restoresettings.html.php b/module/instance/view/restoresettings.html.php new file mode 100644 index 0000000000..02412b1f31 --- /dev/null +++ b/module/instance/view/restoresettings.html.php @@ -0,0 +1,55 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.cn + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +app->getModuleRoot() . '/common/view/datepicker.html.php';?> +instance->notices);?> +id);?> +
+
+
+

instance->restore->autoRestore;?>

+
+
+
+

+ $lang->instance->restore->enableAutoRestore), $restoreSettings->autoRestore ? 'true' : '');?> +

+ + + + + + + + + + + + + +
instance->restore->restoreTime;?> +
+
+ restoreTime, "class='form-control form-time' data-picker-position='bottom-right' maxlength='20'");?> +
+
+
instance->restore->cycleDays;?> +
+ instance->restore->cycleList, $restoreSettings->cycleDays, "class='form-control' maxlength='20'");?> +
+
+
save, "id='saveSetting'", 'btn btn-primary');?>
+
+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/instance/view/tosenior.html.php b/module/instance/view/tosenior.html.php new file mode 100644 index 0000000000..349ca35546 --- /dev/null +++ b/module/instance/view/tosenior.html.php @@ -0,0 +1,28 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.cn + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +
+
+

instance->toSeniorAttention;?>

+
+
+
id}&seniorAppID={$cloudApp->id}&confirm=yes");?>" class="cell not-watch load-indicator main-form form-ajax"> +
instance->toSeniorTips;?>
+
+ $lang->instance->hasRead));?> +
+
instance->upgrade, 'disabled');?>
+
+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/instance/view/upgrade.html.php b/module/instance/view/upgrade.html.php new file mode 100644 index 0000000000..0aa14ee7f5 --- /dev/null +++ b/module/instance/view/upgrade.html.php @@ -0,0 +1,32 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.com + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +instance->notices);?> +
+
+

instance->upgrade} : {$instance->name}";?>

+
+
+ latestVersion)):?> + + + + +
+ + instance->upgrade);?> +
+ +
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/instance/view/view.html.php b/module/instance/view/view.html.php new file mode 100644 index 0000000000..83511316be --- /dev/null +++ b/module/instance/view/view.html.php @@ -0,0 +1,50 @@ + + * @package instance + * @version $Id$ + * @link https://www.qucheng.com + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +instance->notices);?> +id));?> +demoAppLife);?> + + +app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/space/control.php b/module/space/control.php new file mode 100644 index 0000000000..a6541ee60a --- /dev/null +++ b/module/space/control.php @@ -0,0 +1,64 @@ + + * @package space + * @version $Id$ + * @link https://www.qucheng.com + */ +class space extends control +{ + /** + * Browse departments and users of a space. + * + * @param int $param + * @param string $type + * @param string $orderBy + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + @access public + * @return void + */ + public function browse($spaceID = null, $browseType = 'all', $recTotal = 0, $recPerPage = 24, $pageID = 1) + { + $this->app->loadLang('instance'); + $this->loadModel('instance'); + $this->loadModel('store'); + + $spaceType = $this->cookie->spaceType ? $this->cookie->spaceType : 'bycard'; + + $space = null; + if($spaceID) $space = $this->space->getByID($spaceID); + if(empty($space)) $space = $this->space->defaultSpace($this->app->user->account); + + $search = ''; + if(!empty($_POST)) + { + $conditions = fixer::input('post') + ->trim('search') + ->setDefault('search', '') + ->get(); + $search = $conditions->search; + } + + $this->app->loadClass('pager', true); + $pager = pager::init($recTotal, $recPerPage, $pageID); + + $instances = $this->space->getSpaceInstances($space->id, $browseType, $search, $pager); + + $this->view->title = $this->lang->space->common; + $this->view->position[] = $this->lang->space->common; + $this->view->pager = $pager; + $this->view->browseType = $browseType; + $this->view->spaceType = $spaceType; + $this->view->instances = $instances; + $this->view->currentSpace = $space; + $this->view->searchName = $search; + + $this->display(); + } +} diff --git a/module/space/css/browse.css b/module/space/css/browse.css new file mode 100644 index 0000000000..fee40be048 --- /dev/null +++ b/module/space/css/browse.css @@ -0,0 +1,20 @@ +#dropMenu .col-left {padding-bottom: 0;} +input.search-input#search {padding:5px;} +.q-card-title {padding-left: 15px; margin-left: -15px; line-height: 18px; font-size: 16px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; } +#cardsContainer .row {min-height: calc(100vh - 160px);} +#cardsContainer .panel{border-radius: 2px;} +#cardsContainer .panel-heading {padding: 14px 14px 20px 14px;} +#cardsContainer .panel-body {padding: 0 14px;} +#cardsContainer .panel-footer .count-down{display: inline-block; margin: 0 10px;} +#cardsContainer .instance-detail {height: 44px; margin-bottom: 6px;} +#cardsContainer .instance-logo {float: left;} +#cardsContainer .instance-logo img {width: 44px; height: 44px;} +#cardsContainer .instance-intro {float: left; padding-left: 20px; margin-bottom: 0; line-height: 22px; width: calc(100% - 44px); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; vertical-align: middle;} +#cardsContainer .instance-actions {padding: 14px 0 0 0;} +#cardsContainer .instance-footer {height: 50px; padding: 14px; font-size: 13px; font-weight: 600;} +#cardsContainer .instance-footer span {display: inline-block;} +.input-group-btn {float: left;} +#spaceSearchForm > div.btn-toolbar.pull-left > a {padding: 3px;} +#spaceSearchForm button[type=submit] {height: 32px;} +#spaceSearchForm a.btn {margin-top: 3px;} +.solution-link {line-height: 18px; text-align: center; max-width: 140px; display: inline-block; float: right;} diff --git a/module/space/js/browse.js b/module/space/js/browse.js new file mode 100644 index 0000000000..6863ba967c --- /dev/null +++ b/module/space/js/browse.js @@ -0,0 +1,133 @@ +$(function() +{ + $(".switchButton").on('click', function() + { + var spaceType = $(this).attr('data-type'); + $.cookie('spaceType', spaceType, {expires:config.cookieLife, path:config.webRoot}); + }); + $('.btn-uninstall').on('click', function(event) + { + bootbox.confirm(instanceNotices.confirmUninstall, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.uninstalling + '
', + }); + + let id = $(event.target).closest('button').attr('instance-id'); + let url = createLink('instance', 'ajaxUninstall', 'id=' + id, 'json'); + $.post(url).done(function(response) + { + loadingDialog.modal('hide'); + + let res = JSON.parse(response); + if(res.result == 'success') + { + window.location.reload(); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('.btn-start').on('click', function(event) + { + bootbox.confirm(instanceNotices.confirmStart, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.starting + '
', + }); + + let id = $(event.target).closest('button').attr('instance-id'); + let url = createLink('instance', 'ajaxStart', 'id=' + id, 'json'); + $.post(url).done(function(response) + { + loadingDialog.modal('hide'); + + let res = JSON.parse(response); + if(res.result == 'success') + { + window.location.reload(); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } + }); + }); + }); + + $('.btn-stop').on('click', function(event) + { + bootbox.confirm(instanceNotices.confirmStop, function(result) + { + if(!result) return; + + var loadingDialog = bootbox.dialog( + { + message: '
  ' + instanceNotices.stopping + '
', + }); + + let id = $(event.target).closest('button').attr('instance-id'); + let url = createLink('instance', 'ajaxStop', 'id=' + id, 'json'); + $.post(url).done(function(response) + { + loadingDialog.modal('hide'); + + let res = JSON.parse(response); + if(res.result == 'success') + { + window.location.reload(); + } + else + { + bootbox.alert( + { + title: instanceNotices.fail, + message: res.message, + }); + } }); + }); + }); + + var enableTimer = true; + window.parent.$(window.parent.document).on('showapp', function(event, app) + { + enableTimer = app.code == 'space'; + }); + + setInterval(function() + { + if(!enableTimer) return; + + var statusURL = createLink('instance', 'ajaxStatus'); + $.post(statusURL, {idList: instanceIdList}).done(function(response) + { + let res = JSON.parse(response); + if(res.result == 'success' && res.data instanceof Array) + { + res.data.forEach(function(instance) + { + if($(".instance-status[instance-id=" + instance.id + "]").data('status') != instance.status) window.location.reload(); + }); + } + if(res.locate) window.parent.location.href = res.locate; + }); + }, 1000 * 5); +}) diff --git a/module/space/js/common.js b/module/space/js/common.js new file mode 100644 index 0000000000..940461c4ed --- /dev/null +++ b/module/space/js/common.js @@ -0,0 +1,4 @@ +$(function() +{ + window.parent.$.apps.close('store'); +}); diff --git a/module/space/lang/zh-cn.php b/module/space/lang/zh-cn.php new file mode 100644 index 0000000000..d2f9ea809f --- /dev/null +++ b/module/space/lang/zh-cn.php @@ -0,0 +1,20 @@ +space->status = '状态'; +$lang->space->noApps = '暂无服务'; +$lang->space->defaultSpace = '默认空间'; +$lang->space->systemSpace = '系统空间'; +$lang->space->searchInstance = '搜索服务'; +$lang->space->upgrade = '升级'; +$lang->space->install = '添加应用'; + +$lang->space->notice = new stdclass; +$lang->space->notice->toInstall = '请到应用市场安装'; + +$lang->space->byList = '列表'; +$lang->space->byCard = '卡片'; + +$lang->space->filterList['all'] = '全部'; +//$lang->space->filterList['toBeUpdated'] = '待更新'; +$lang->space->filterList['running'] = '运行中'; +$lang->space->filterList['stopped'] = '已停止'; +$lang->space->filterList['abnormal'] = '异常'; diff --git a/module/space/model.php b/module/space/model.php new file mode 100644 index 0000000000..6b5e430439 --- /dev/null +++ b/module/space/model.php @@ -0,0 +1,132 @@ + + * @package space + * @version $Id$ + * @link https://www.qucheng.com + */ +class spaceModel extends model +{ + /** + * Get space list by user account. + * + * @param string $account + * @access public + * @return array + */ + public function getSpacesByAccount($account) + { + return $this->dao->select('*')->from(TABLE_SPACE)->where('deleted')->eq(0)->andWhere('owner')->eq($account)->fetchAll(); + } + + /** + * Get user's default space by user account. + * + * @param string $account + * @access public + * @return object + */ + public function defaultSpace($account) + { + $default = $this->dao->select('*')->from(TABLE_SPACE)->where('deleted')->eq(0)->andWhere('owner')->eq($account)->orderBy('default desc')->limit(1)->fetch(); + + if(empty($default)) return $this->createDefaultSpace($account); + return $default; + } + + /** + * Get system space. + * + * @param string $account + * @access public + * @return object + */ + public function getSystemSpace($account) + { + $k8space = $this->config->k8space; + + $sysSpace = $this->dao->select('*')->from(TABLE_SPACE)->where('k8space')->eq($k8space)->andWhere('owner')->eq($account)->fetch(); + if($sysSpace) return $sysSpace; + + $spaceData = new stdClass; + $spaceData->name = $this->lang->space->systemSpace; + $spaceData->owner = $account; + $spaceData->k8space = $k8space; + $spaceData->default = 0; + $spaceData->createdAt = date('Y-m-d H:i:s'); + + $this->dao->insert(TABLE_SPACE)->data($spaceData)->autoCheck()->exec(); + + return $this->dao->select('*')->from(TABLE_SPACE)->where('id')->eq($this->dao->lastInsertId())->fetch(); + } + + /** + * Create default space by account + * + * @param string $account + * @access public + * @return object + */ + public function createDefaultSpace($account) + { + $default = new stdclass; + $default->name = $this->lang->space->defaultSpace; + $default->k8space = 'quickon-app'; + $default->owner = $account; + $default->default = true; + $default->createdAt = date('Y-m-d H:i:s'); + + $this->dao->insert(TABLE_SPACE)->data($default)->autoCheck()->exec(); + + return $this->dao->select('*')->from(TABLE_SPACE)->where('id')->eq($this->dao->lastInsertId())->fetch(); + } + + /** + * Get app list in space + * + * @param int $spaceID + * @param object $pager + * @access public + * @return array + */ + public function getSpaceInstances($spaceID, $status = 'all', $searchName = '', $pager = null) + { + $space = $this->dao->select('*')->from(TABLE_SPACE)->where('deleted')->eq(0)->andWhere('id')->eq($spaceID)->fetch(); + + $instances = $this->dao->select('*')->from(TABLE_INSTANCE) + ->where('deleted')->eq(0) + ->andWhere('space')->eq($spaceID) + ->beginIF($status !== 'all')->andWhere('status')->eq($status)->fi() + ->beginIF(!empty($searchName))->andWhere('name')->like("%{$searchName}%")->fi() + ->orderBy('id desc')->page($pager)->fetchAll('id'); + + $this->loadModel('store'); + foreach($instances as $instance) $instance->latestVersion = $this->store->appLatestVersion($instance->appID, $instance->version); + + $solutionIDList = array_column($instances, 'solution'); + $solutions = $this->dao->select('*')->from(TABLE_SOLUTION)->where('id')->in($solutionIDList)->fetchAll('id'); + foreach($instances as $instance) $instance->solutionData = zget($solutions, $instance->solution, new stdclass); + + return $instances; + } + + /** + * Get space by id. + * + * @param int $id + * @access public + * @return object + */ + public function getByID($id) + { + return $this->dao->select('*')->from(TABLE_SPACE) + ->where('deleted')->eq(0) + ->andWhere('id')->eq($id) + ->andWhere('owner')->eq($this->app->user->account) + ->fetch(); + } +} diff --git a/module/space/view/ajaxgetdropmenu.html.php b/module/space/view/ajaxgetdropmenu.html.php new file mode 100644 index 0000000000..5c82847d8d --- /dev/null +++ b/module/space/view/ajaxgetdropmenu.html.php @@ -0,0 +1,16 @@ + +
+
+
+
    + +
  • id"), $space->name, '', 'class="search-list-item ' . ($space->id == $spaceID ? 'selected' : '') . '"');?>
  • + +
+
+
+
diff --git a/module/space/view/browse.html.php b/module/space/view/browse.html.php new file mode 100644 index 0000000000..429524a66c --- /dev/null +++ b/module/space/view/browse.html.php @@ -0,0 +1,54 @@ + + * @package space + * @version $Id$ + * @link https://www.qucheng.com + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +instance->notices);?> + + +
+ +
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/space/view/browsebycard.html.php b/module/space/view/browsebycard.html.php new file mode 100644 index 0000000000..0a1449d120 --- /dev/null +++ b/module/space/view/browsebycard.html.php @@ -0,0 +1,66 @@ + + * @package space + * @version $Id$ + * @link https://www.qucheng.com + */ +?> +
+ +
+

createLink('store', 'browse'), $lang->space->noApps . ', ' . $lang->space->notice->toInstall, '', "class='btn btn-info'");?>

+
+ +
+ +
+
+
+ + latestVersion) && empty($instance->solution)):?> +
latestVersion->change_log_url) ? $lang->space->upgrade : html::a($instance->latestVersion->change_log_url, $lang->space->upgrade, '_blank');?>
+ +
+
+ +
+ instance->canDo('visit', $instance);?> + instance->url($instance), $lang->instance->visit, '_blank', "class='btn btn-primary' title='{$lang->instance->visit}'". ($canVisit ? '' : ' disabled style="pointer-events: none;"'));?> + latestVersion) && empty($instance->solution)):?> + id", '', true), "" . $lang->space->upgrade, '', "class='btn btn-link iframe' title='{$lang->space->upgrade}' data-width='500' data-app='space'");?> + + solution):?> + solution"), $instance->solutionData->name, '', "title='{$instance->solutionData->name}' class='label label-success label-outline solution-link text-ellipsis'");?> + +
+
+ +
+
+ +
+ + +
diff --git a/module/space/view/browsebylist.html.php b/module/space/view/browsebylist.html.php new file mode 100644 index 0000000000..5d72f2e362 --- /dev/null +++ b/module/space/view/browsebylist.html.php @@ -0,0 +1,56 @@ + + * @package space + * @version $Id$ + * @link https://www.qucheng.net + */ +?> +
+ +
+

createLink('store', 'browse'), $lang->space->noApps . ', ' . $lang->space->notice->toInstall, '', "class='btn btn-info'");?>

+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
instance->name;?>instance->appName;?>version;?>space->status;?>operation;?>
createLink('instance', 'view', "id=$instance->id"), $instance->name);?>createLink('store', 'appview', "id=$instance->appID"), $instance->appName);?>appVersion;?> + instance->printStatus($instance);?> + + instance->printIconActions($instance);?> + latestVersion)):?> + id", '', true), "", '', "class='btn btn-action iframe' title='{$lang->space->upgrade}' data-width='500' data-app='space'");?> + +
+ + + +
+ +
diff --git a/module/store/config.php b/module/store/config.php new file mode 100644 index 0000000000..174589f29e --- /dev/null +++ b/module/store/config.php @@ -0,0 +1,3 @@ +store = new stdclass; +$config->store->quchengSiteHost = 'https://qucheng.com'; diff --git a/module/store/control.php b/module/store/control.php new file mode 100644 index 0000000000..58373c491a --- /dev/null +++ b/module/store/control.php @@ -0,0 +1,128 @@ + + * @package store + * @version $Id$ + * @link https://www.qucheng.com + */ +class store extends control +{ + /** + * Contruct function, load cne model. + * + * @access public + * @return void + */ + public function __construct() + { + parent::__construct(); + $this->loadModel('cne'); + $this->app->loadLang('instance'); + } + + /** + * Index page. + * + * @access public + * @return void + */ + public function index() + { + $this->locate($this->createLink('store', 'browse')); + } + + /** + * Browse departments and users of a store. + * + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + * @param string $channel + * @access public + * @return void + */ + public function browse($sortType = 'create_time', $recPerPage = 0, $pageID = 1, $channel = '') + { + global $config; + if(empty($recPerPage)) $recPerPage = $this->cookie->pagerStoreBrowse ? $this->cookie->pagerStoreBrowse : 24; + if(in_array( $channel, array('stable', 'test'))) + { + $config->CNE->api->channel = $channel; + $config->cloud->api->channel = $channel; + $this->session->set('cloudChannel', $channel); + } + + $keyword = ''; + $postCategories = array(); + if(!empty($_POST)) + { + $pageID = 1; + $conditions = fixer::input('post') + ->setDefault('keyword', '') + ->setDefault('categories', array()) + ->get(); + $keyword = $conditions->keyword; + $postCategories = $conditions->categories; + } + + $pagedApps = $this->store->searchApps($sortType, $keyword, $postCategories, $pageID, $recPerPage); + $this->app->loadClass('pager', true); + $pager = pager::init($pagedApps->total, $recPerPage, $pageID); + + $pagedCategories = $this->store->getCategories(); + $categories = array_combine(array_column($pagedCategories->categories, 'id'), array_column($pagedCategories->categories, 'alias')); + + $this->lang->switcherMenu = $this->store->getBrowseSwitcher(); + + $this->view->title = $this->lang->store->common; + $this->view->position[] = $this->lang->store->common; + $this->view->cloudApps = $pagedApps->apps; + $this->view->categories = $categories; + $this->view->postCategories = $postCategories; + $this->view->keyword = $keyword; + $this->view->sortType = $sortType; + $this->view->pager = $pager; + + $this->display(); + } + + /** + * Show app detail. + * + * @param int $id + * @access public + * @return viod + */ + public function appView($id, $pageID = 1, $recPerPage = 20) + { + $appInfo = $this->store->getAppInfo($id, true); + if(empty($appInfo)) return print(js::locate('back', 'parent')); + + $this->lang->switcherMenu = $this->store->getAppViewSwitcher($appInfo); + + $dynamicResult = $this->store->appDynamic($appInfo, $pageID, $recPerPage); + $articles = array(); + $totalArticle = 0; + if(!empty($dynamicResult)) + { + $articles = $dynamicResult->articles; + $totalArticle = $dynamicResult->recTotal; + } + $this->view->dynamicArticles = $articles; + + $this->app->loadClass('pager', true); + $pager = pager::init($totalArticle, $recPerPage, $pageID); + $this->view->dynamicPager = $pager; + + $this->view->title = $appInfo->alias; + $this->view->position[] = $appInfo->alias; + $this->view->cloudApp = $appInfo; + $this->view->components = null; // Hide custom installation in version 1.0. If want, opened by: $this->store->getAppSettings($id); + + $this->display(); + } +} diff --git a/module/store/css/appview.css b/module/store/css/appview.css new file mode 100644 index 0000000000..100ea476d8 --- /dev/null +++ b/module/store/css/appview.css @@ -0,0 +1,19 @@ +.app-logo div {text-align: right; padding-right: 8px;} +.app-logo img {width: 48px; height: 48px;} +table tr td.app-desc {vertical-align: top;} +table tr td.app-action {vertical-align: top; text-align: center;} +#mainContent .cell{padding: 20px;} +#mainContent .app-logo img {width: 50px; height: 50px; display: inline-block; margin-right: 20px;} +#mainContent hr {margin: 10px 0 0 0;} +#appInfoHeader {height: 60px;} +#appInfoHeader button .icon{color: #70c0fb;} +#appInfoHeader .btn {display: inline-block; margin: 0 10px;} +#appInfoHeader .icon:before {padding-right: 5px; font-size: 18px;} +#appInfoHeader .icon-docker:before {color: #2196f3;} +#appInfoHeader .icon-forum:before {color: #ff9800;} +#appInfoHeader .dropdown .icon {padding-right: 5px;} +#appInfoBody h3 {margin-top: 40px;} +.dynamicContainer {padding: 10px; border: solid 1px #ddd;} +.dynamicContainer table {margin-bottom: 0;} +.screenshotsContainer .errorBox{padding: 20px; border: solid 1px #ddd;} +.screenshotsContainer img{width: 100%; height: auto; border: solid 1px #ddd; border-radius: 3px; padding: 5px;} diff --git a/module/store/css/browse.css b/module/store/css/browse.css new file mode 100644 index 0000000000..145a1a7ade --- /dev/null +++ b/module/store/css/browse.css @@ -0,0 +1,17 @@ +#appSearchForm input[type=text] {padding: 5px 10px;} +#appSearchForm button[type=submit] {height: 32px;} +#appContainer .row {min-height: calc(100vh - 130px);} +#appContainer .card a:hover {color: #0b89b2;} +#appContainer .card.panel {border-radius: 2px;} +#appContainer .panel-heading {padding-right: 20px;} +#appContainer .app-name {font-size: 18px; font-weight: 700; display: inline-block;padding: 8px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle;} +#appContainer .app-detail {height: 80px;} +#appContainer .app-logo {float: left;} +#appContainer .app-logo img {width: 80px; height: 80px;} +#appContainer .app-desc {float: left; padding-left: 20px; width: calc(100% - 80px);} +#appContainer .app-desc {display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; vertical-align: middle;} +#appContainer .app-footer {height: 50px; padding: 10px 20px; font-size: 14px; font-weight: bold;} +#appContainer .app-footer span {display: inline-block; margin: 0 10px;} +#appContainer .pagination ul {float: right;} +#appContainer .btn-toolbar {height: 40px;} +.sort {padding: 0 10px;} diff --git a/module/store/js/browse.js b/module/store/js/browse.js new file mode 100644 index 0000000000..b496914f29 --- /dev/null +++ b/module/store/js/browse.js @@ -0,0 +1,6 @@ +$(function() +{ + $('#appSearchForm #submit').attr('id', ''); + + $("input[name='categories[]']").on('change', function(){$('#appSearchForm').submit();}); +}); diff --git a/module/store/lang/zh-cn.php b/module/store/lang/zh-cn.php new file mode 100644 index 0000000000..a8630fa27c --- /dev/null +++ b/module/store/lang/zh-cn.php @@ -0,0 +1,28 @@ +store->appType = '应用类别'; +$lang->store->appVersion = '版本'; +$lang->store->releaseDate = '发布日期'; +$lang->store->author = '发布者'; +$lang->store->empty = '暂无应用'; +$lang->store->install = '安装'; +$lang->store->customInstall = '自定义安装'; +$lang->store->searchApp = '查找应用'; +$lang->store->testChannel = '测试版'; +$lang->store->stableChannel = '稳定版'; +$lang->store->appBasicInfo = '基本信息'; +$lang->store->screenshots = '应用截图'; +$lang->store->appDynamic = '应用动态'; +$lang->store->noDynamicArticle = '暂无动态'; +$lang->store->noScreenshot = '暂无截图'; +$lang->store->cloudStore = '应用市场'; + +$lang->store->support = '支持'; +$lang->store->gitUrl = '应用源码'; +$lang->store->dockerfileUrl = 'Dockerfile'; +$lang->store->forumUrl = '论坛'; + +$lang->store->sortTypes = array(); +$lang->store->sortTypes['id'] = '默认'; +$lang->store->sortTypes['create_time'] = '最新上架'; +$lang->store->sortTypes['update_time'] = '最近更新'; +//$lang->store->sortTypes['name'] = '名称'; diff --git a/module/store/model.php b/module/store/model.php new file mode 100644 index 0000000000..87d716d35a --- /dev/null +++ b/module/store/model.php @@ -0,0 +1,409 @@ + + * @package store + * @version $Id$ + * @link https://www.qucheng.com + */ +class storeModel extends model +{ + /** + * Construct function: set api headers. + * + * @param string $appName + * @access public + * @return void + */ + public function __construct($appName = '') + { + parent::__construct($appName); + + global $config, $app; + $config->cloud->api->headers[] = "{$config->cloud->api->auth}: {$config->cloud->api->token}"; + + if($config->cloud->api->switchChannel && $app->session->cloudChannel) $config->cloud->api->channel = $app->session->cloudChannel; + } + + /** + * Get app list from cloud market. + * + * @param string $keyword + * @param array $categories + * @param int $page + * @param int $pageSize + * @access public + * @return object + */ + public function searchApps($sortBy = '', $keyword = '', $categories = array(), $page = 1, $pageSize = 20) + { + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/applist?channel='. $this->config->cloud->api->channel; + $apiUrl .= "&q=" . rawurlencode(trim($keyword)); + $apiUrl .= "&sort=" . rawurlencode(trim($sortBy)); + $apiUrl .= "&page=$page"; + $apiUrl .= "&page_size=$pageSize"; + foreach($categories as $category) $apiUrl .= "&category=$category"; // The names of category are same that reason is CNE api is required. + + $result = commonModel::apiGet($apiUrl, array(), $this->config->cloud->api->headers); + if($result->code == 200) return $result->data; + + $pagedApps = new stdclass; + $pagedApps->apps = array(); + $pagedApps->total = 0; + return $pagedApps; + } + + /** + * Get app info by App chart from cloud market. + * + @param string $chart + * @param string $channel + * @param bool $analysis + * @param string $version + * @access public + * @return object|null + */ + public function getAppInfoByChart($chart, $channel, $analysis, $version = '') + { + return $this->getAppInfo(0, $analysis, $chart, $version, $channel); + } + + /** + * Get app info from cloud market. + * + * @param int $id + * @param boolean $analysis true: log this request for analysis. + * @param string $name + * @param string $version + * @param string $channel + * @access public + * @return object|null + */ + public function getAppInfo($id, $analysis = false, $name = '', $version ='', $channel = '') + { + $apiParams = array(); + $apiParams['analysis'] = $analysis ? 'true' : 'false' ; + + if($id) $apiParams['id'] = $id; + if($name) $apiParams['name'] = $name; + if($version) $apiParams['version'] = $version; + if($channel) $apiParams['channel'] = $channel; + + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/appinfo'; + $result = commonModel::apiGet($apiUrl, $apiParams, $this->config->cloud->api->headers); + if(!isset($result->code) || $result->code != 200) return null; + + return $result->data; + } + + /** + * Get app version list to install. + * + * @param int $id + * @param string $name + * @param string $channel + * @param int $page + * @param int $pageSize + * @access public + * @return mixed + */ + public function appVersionList($id, $name = '', $channel = '', $page = 1, $pageSize = 3) + { + $apiParams = array(); + $apiParams['page'] = $page; + $apiParams['page_size'] = $pageSize; + + if($id) $apiParams['id'] = $id; + if($name) $apiParams['name'] = $name; + if($channel) $apiParams['channel'] = $channel; + + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/app/version'; + $result = commonModel::apiGet($apiUrl, $apiParams, $this->config->cloud->api->headers); + if(!isset($result->code) || $result->code != 200) return null; + + return array_combine(array_column($result->data, 'version'), $result->data); + } + + /** + * Get upgradable versions of app from cloud market. + * + * @param string $currentVersion + * @param int $appID appID is required if no appName. + * @param string $appName appName is required if no appID. + * @param string $channel + * @access public + * @return mixed + */ + public function getUpgradableVersions($currentVersion, $appID = 0, $appName = '', $channel = '') + { + $channel = $channel ? $channel : $this->config->cloud->api->channel; + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/app/version/upgradable'; + + $conditions = array('version' => $currentVersion, 'channel' => $channel); + if($appID) + { + $conditions['id'] = $appID; + } + else + { + $conditions['name'] = $appName; + } + + $result = commonModel::apiGet($apiUrl, $conditions, $this->config->cloud->api->headers); + if(!isset($result->code) || $result->code != 200) return array(); + + return $result->data; + } + + /** + * Get the latest version of QuCheng platform. + * + * @access public + * @return object + */ + public function platformLatestVersion() + { + $versionList = $this->getUpgradableVersions($this->config->platformVersion, 0, $this->config->edition == 'open' ? 'qucheng' : 'qucheng-biz', $this->config->cloud->api->channel); + + $latestVersion = $this->pickHighestVersion($versionList); + if(!empty($latestVersion) && version_compare(str_replace('-', '.', $latestVersion->version), str_replace('-', '.', $this->config->platformVersion), '>')) return $latestVersion; + + $latestVersion = new stdclass; + $latestVersion->app_version = getenv('APP_VERSION'); + $latestVersion->version = $this->config->platformVersion; + + return $latestVersion; + } + + /** + * Get the latest versions of app from cloud market. + * + * @param int $appID + * @param string $currentVersion + * @access public + * @return object|null + */ + public function appLatestVersion($appID, $currentVersion) + { + $versionList = $this->getUpgradableVersions($currentVersion, $appID); + + $latestVersion = $this->pickHighestVersion($versionList); + if(empty($latestVersion)) return null; + + if(version_compare(str_replace('-', '.', $latestVersion->version), str_replace('-', '.', $currentVersion), '>')) return $latestVersion; + + return null; + } + + /** + * Pick highest version from version list and compared version. + * + * @param array $versionList + * @access private + * @return object|null + */ + private function pickHighestVersion($versionList) + { + if(empty($versionList)) return null; + + $highestVersion = new stdclass; + $highestVersion->version = '0.0.0'; + foreach($versionList as $version) + { + if(version_compare(str_replace('-', '.', $version->version), str_replace('-', '.', $highestVersion->version), '>')) $highestVersion = $version; + } + + return $highestVersion; + } + + /** + * Get app setting from cloud market. + * + * @param int $id + * @access public + * @return array + */ + public function getAppSettings($id) + { + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/appsettings'; + $result = commonModel::apiGet($apiUrl, array('id' => $id), $this->config->cloud->api->headers); + if($result->code != 200) return array(); + + /* Convert "." to "_" */ + $components = $result->data->components; + foreach($result->data->components as &$component) + { + foreach($component->settings as $setting) $setting->field = str_replace('.', '_', $setting->field); + } + + return $components; + } + + /** + * Get category list from cloud market. + * + * @access public + * @return object + */ + public function getCategories() + { + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/categories'; + $result = commonModel::apiGet($apiUrl, array(), $this->config->cloud->api->headers); + if($result->code == 200) return $result->data; + + $categories = new stdclass; + $categories->categories = array(); + $categories->total = 0; + return $categories; + } + + /** + * Get switcher of browse page of store. + * + * @access public + * @return string + */ + public function getBrowseSwitcher() + { + $title = $this->lang->store->cloudStore; + + if($this->config->cloud->api->switchChannel) $title .= '(' . ($this->config->cloud->api->channel == 'stable' ? $this->lang->store->stableChannel : $this->lang->store->testChannel) . ')'; + + $output = "
"; + if($this->config->cloud->api->switchChannel) + { + $stableActive = $this->config->cloud->api->channel == 'stable' ? 'active' : ''; + $testActive = $this->config->cloud->api->channel != 'stable' ? 'active' : ''; + + $output .= "{$title}"; + $output .= ""; + } + else + { + $output .= "{$title}"; + } + + $output .= "
"; + + return $output; + } + + /** + * Get switcher of app view page of store. + * + * @param object $app + * @access public + * @return string + */ + public function getAppViewSwitcher($app) + { + $output = $this->getBrowseSwitcher(); + $output .= "
"; + $output .= html::a(helper::createLink('store', 'appview', "id=$app->id"), $app->alias, '', 'class="btn"'); + $output .= "
"; + + return $output; + } + + /** + * Get app dynamic news from Qucheng offical site. + * + * @param object $cloudApp + * @param int $pageID + * @param int $recPerPage + * @access public + * @return object|null + */ + public function appDynamic($cloudApp, $pageID = 1, $recPerPage = 20) + { + $alias = strtolower(str_replace('-', '', $cloudApp->chart)); + $url = $this->config->store->quchengSiteHost . "/article-apibrowse-{$alias}-{$pageID}-{$recPerPage}.html"; + + $result = commonModel::apiGet($url); + if($result && $result->code == 200) return $result->data; + + return null; + } + + /** + * Get solution list. + * + * @param string $sortBy possible vlues: id,name,create_time,update_time + * @param string $keyword + * @param int $page + * @param int $pageSize + * @access public + * @return object + */ + public function searchSolutions($sortBy = '', $keyword = '', $page = 1, $pageSize = 20) + { + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/solution/list?channel='. $this->config->cloud->api->channel; + $apiUrl .= "&sort=" . rawurlencode(trim($sortBy)); + $apiUrl .= "&q=" . rawurlencode(trim($keyword)); + $apiUrl .= "&page=$page"; + $apiUrl .= "&page_size=$pageSize"; + + $result = commonModel::apiGet($apiUrl, array(), $this->config->cloud->api->headers); + if($result->code == 200) return $result->data; + + $pagedApps = new stdclass; + $pagedApps->apps = array(); + $pagedApps->total = 0; + return $pagedApps; + } + + /** + * Get solution info by ID. + * + * @param int $id + * @access public + * @return object + */ + public function getSolutionByID($id) + { + $apiParams = array(); + $apiParams['id'] = $id; + + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/solution/info?channel='. $this->config->cloud->api->channel; + $result = commonModel::apiGet($apiUrl, $apiParams, $this->config->cloud->api->headers); + if(!isset($result->code) || $result->code != 200) return null; + + $solution = $result->data; + $solution->apps = array_combine(array_column($solution->apps, 'chart'), $solution->apps); + + return $solution; + } + /** + * Get solution config by ID. + * + * @param int $id + * @access public + * @return object + */ + public function solutionConfigByID($id) + { + $apiParams = array(); + $apiParams['id'] = $id; + + $apiUrl = $this->config->cloud->api->host; + $apiUrl .= '/api/market/solution/schema?channel='. $this->config->cloud->api->channel; + $result = commonModel::apiGet($apiUrl, $apiParams, $this->config->cloud->api->headers); + if(!isset($result->code) || $result->code != 200) return null; + + return $result->data; + } +} diff --git a/module/store/view/appview.html.php b/module/store/view/appview.html.php new file mode 100644 index 0000000000..1fce38b6a2 --- /dev/null +++ b/module/store/view/appview.html.php @@ -0,0 +1,107 @@ + + * @package store + * @version $Id$ + * @link https://www.qucheng.com + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +instance->notices);?> + + +
+
+
+ + +
+ +
+
+
+
+ +
+

store->empty;?>

+
+ +
+
+

store->appBasicInfo;?>

+

desc;?>

+ + + + + + + + + + + + + + + +
store->appVersion;?>:app_version;?>store->author;?>:author;?>
store->releaseDate;?>:publish_time))->format('Y-m-d');?>store->appType;?>:categories, 'alias')), '/');?>
+

store->screenshots;?>

+
+ screenshot_urls))):?> +
store->noScreenshot;?>
+ + screenshot_urls);?> + + +
+ +
+ + +
+
+
+

store->appDynamic;?>

+
+ + + + + + + + +
store->noDynamicArticle;?>
url, $article->title, "target='_blank'");?>
+ + + +
+
+
+ +
+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?> diff --git a/module/store/view/browse.html.php b/module/store/view/browse.html.php new file mode 100644 index 0000000000..108250915d --- /dev/null +++ b/module/store/view/browse.html.php @@ -0,0 +1,76 @@ + + * @package store + * @version $Id$ + * @link https://www.qucheng.com + */ +?> +app->getModuleRoot() . '/common/view/header.html.php';?> +
+ +
+
+
+ store->sortTypes as $sortCode => $sortLabel):?> + + $sortLabel";?> + + +
+
+ +
+

store->empty;?>

+
+ + + + +
+
+app->getModuleRoot() . '/common/view/footer.html.php';?>