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 = "- 版本升级后,无法回退到原版本。
- 企业版、旗舰版自安装后免费试用6个月。
- 开源版升级到企业版或旗舰版后,试用期最大支持3个用户,
+ 请检查开源版用户数量。超出限制将不可用。
- 升级成功后,服务将自动重启。
- 为避免造成数据丢失,请您在升级前务必做好数据备份。
";
+
+$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 = "";
+
+ 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 .= "";
+
+ 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 .= "";
+
+ 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 @@
+
+
+
+
+
+
+
+
+ features->ldap)):?>
+
+
system->ldapManagement;?>
+
+
+
+ features->mail)):?>
+
+
system->SMTP->common;?>
+
+
+
+
+ scalable) && $config->edition == 'biz'):?>
+
+
instance->scalable;?>
+
+
+
+
+
+
+
instance->customSetting;?>
+
+
+
+
+
+
+
+
+
+
+ | 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);?>
+
+
+
+
+
+
+ | 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));?> |
+ 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;?>
+ |
+
+
+ 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);?> |
+
+
+
+
+
+
+
+
+
+
instance->backup->restore;?>
+
+
instance->backup->backupBeforeRestore;?>
+
+ instance->backup->latestBackupAt;?>:
+ create_time);?>
+
+
+
+ instance->backup->common, "instance-id='{$instance->id}'", 'btn-backup btn btn-wide');?>
+ instance->backup->restore, "id='submitRestore'", 'btn btn-primary btn-wide');?>
+
+
+
+
+
+
+
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;?>
+
+
+
+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 @@
+
+
+
+
+
+
+
+
+ | 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);?> |
+
+ status == 'running'):?>
+
+ | 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';?>
+
+
+ createLink('store', 'appView', "id={$appID}"), ' ' . $lang->goback, '', "class='btn btn-secondary'");?>
+
+
+
+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;?>
+
+
+
+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;?>
+
+
+
+
+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;?>
+
+
+
+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;?>
+
+
+
+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}";?>
+
+
+
+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);?>
+
+
+
name;?>
+ appVersion;?>
+ createLink('instance', 'editname', "id=$instance->id", '', true), '', '', "class='iframe' title='$lang->edit' data-width='600' data-app='space'");?>
+ solution):?>
+ solution"), $instance->solutionData->name, '', "class='label label-success label-outline solution-link'");?>
+
+
+
+
+
+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'");?>
+
+
+
+
+
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 = "";
+
+ 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 .= "";
+
+ 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);?>
+
+
+
+ createLink('store', 'browse'), ' ' . $lang->goback, '', "class='btn btn-secondary'");?>
+
+
+
+
+
+
+
+
+
+
+
+
+
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))):?>
+
+
+ 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";?>
+
+
+
+
+
+
+
+
+
+
+
+
+app->getModuleRoot() . '/common/view/footer.html.php';?>