index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <div>
  3. <page-header :title="$t('helm.text_25')" />
  4. <page-body needMarginBottom>
  5. <div>
  6. <template v-if="!chartDetail.name">
  7. <loading-block :layout="loadingLayout" />
  8. </template>
  9. <template v-else>
  10. <page-card-detail
  11. class="mt-3 mb-4"
  12. :img="chartDetail.chart.metadata.icon"
  13. :page-title="chartDetail.name"
  14. :description="chartDetail.chart.metadata.description">
  15. <div class="mt-1" style="font-size: 12px; color: #555; font-weight: 500;">{{ isVm ? $t('helm.text_26') : $t('helm.text_27') }}</div>
  16. </page-card-detail>
  17. <a-form
  18. v-bind="formItemLayout"
  19. :form="form.fc">
  20. <a-form-item :label="$t('helm.text_16')">
  21. <a-input v-decorator="decorators.release_name" :placeholder="$t('helm.text_28')" />
  22. </a-form-item>
  23. <a-form-item :label="$t('helm.text_29')">
  24. <a-select v-decorator="decorators.version" :placeholder="$t('helm.text_30')">
  25. <a-select-option
  26. v-for="item in versions"
  27. :key="item.key"
  28. :value="item.key">
  29. {{ item.label }}
  30. </a-select-option>
  31. </a-select>
  32. </a-form-item>
  33. <template v-if="isVm">
  34. <a-form-item :label="$t('scope.text_573', [$t('dictionary.project')])">
  35. <domain-project :fc="form.fc" :decorators="{ project: decorators.project, domain: decorators.domain }" />
  36. </a-form-item>
  37. </template>
  38. <template v-else>
  39. <a-form-item :label="$t('helm.text_31')">
  40. <cluster-select v-decorator="decorators.cluster" @input="setCluster" />
  41. </a-form-item>
  42. <a-form-item :label="$t('helm.text_32')">
  43. <namespace-select v-decorator="decorators.namespace" @input="setNamespace" :cluster="cluster" :namespaceObj.sync="namespaceObj" />
  44. </a-form-item>
  45. </template>
  46. <a-collapse v-model="activeKey">
  47. <a-collapse-panel :header="$t('helm.text_46')" key="jsonschema">
  48. <json-schema-form v-if="isJsonSchema" :schema="schema" :extendFd="form.fd" :definition="definition" :hide-reset="false" :influxdbUrl="influxdbUrl" ref="formRef" />
  49. <form-yaml
  50. v-else
  51. :decorators="decorators"
  52. :activeTab.sync="formActiveTab"
  53. :localData="chartDetail.chart.values"
  54. :valueSearch="valueSearch" />
  55. </a-collapse-panel>
  56. <a-collapse-panel v-if="chartDetail.yaml !== ''" :header="$t('helm.text_33')" key="desc">
  57. <div v-html="compiledMarkdown" />
  58. </a-collapse-panel>
  59. <a-collapse-panel :header="$t('helm.text_34')" key="yaml" v-if="false">
  60. <template-preview :previewFiles="previewFiles" />
  61. </a-collapse-panel>
  62. </a-collapse>
  63. </a-form>
  64. </template>
  65. </div>
  66. </page-body>
  67. <page-footer>
  68. <div slot="right">
  69. <a-button class="mr-3" type="primary" @click="confirm" :loading="loading">{{$t('helm.text_35')}}</a-button>
  70. <a-button @click="cancel">{{$t('helm.text_36')}}</a-button>
  71. </div>
  72. </page-footer>
  73. </div>
  74. </template>
  75. <script>
  76. import _ from 'lodash'
  77. import * as R from 'ramda'
  78. import marked from 'marked'
  79. import { Base64 } from 'js-base64'
  80. import { mapGetters } from 'vuex'
  81. import ClusterSelect from '@K8S/sections/ClusterSelect'
  82. import NamespaceSelect from '@K8S/sections/NamespaceSelect'
  83. import TemplatePreview from '@K8S/sections/TemplatePreview'
  84. import k8sCreateMixin from '@K8S/mixins/create'
  85. import FormYaml from '@Helm/views/chart/create/FormYaml'
  86. import DomainProject from '@/sections/DomainProject'
  87. import { validateYaml, isRequired } from '@/utils/validate'
  88. import { HYPERVISORS_MAP } from '@/constants'
  89. import { compactObj } from '@/utils/utils'
  90. export default {
  91. name: 'K8SChartCreate',
  92. components: {
  93. ClusterSelect,
  94. NamespaceSelect,
  95. TemplatePreview,
  96. DomainProject,
  97. FormYaml,
  98. },
  99. mixins: [k8sCreateMixin],
  100. data () {
  101. const validator = (rule, value, _callback) => {
  102. validateYaml(value)
  103. .then(() => {
  104. return _callback()
  105. })
  106. .catch(() => {
  107. return _callback(this.$t('helm.text_37'))
  108. })
  109. }
  110. return {
  111. activeKey: ['jsonschema', 'desc', 'yaml'],
  112. formActiveTab: 'form',
  113. isJsonSchema: false,
  114. influxdbUrl: '',
  115. versions: [],
  116. previewFiles: [],
  117. loading: false,
  118. chartDetail: {
  119. readme: '',
  120. chart: {},
  121. },
  122. namespaceObj: {},
  123. formItemLayout: {
  124. labelCol: { span: 4 },
  125. wrapperCol: { span: 20 },
  126. },
  127. isVm: this.$route.query.type === 'internal',
  128. loadingLayout: [
  129. [10],
  130. [8, 9],
  131. [2, 4, 7, 5],
  132. [13, 9],
  133. [4, 3, 8],
  134. [8, 6, 8],
  135. [13, 9],
  136. ],
  137. form: {
  138. fc: this.$form.createForm(this, {
  139. onValuesChange: (props, values) => {
  140. Object.keys(values).forEach((key) => {
  141. this.$set(this.form.fd, key, values[key])
  142. })
  143. },
  144. }),
  145. fd: {},
  146. },
  147. schema: {},
  148. decorators: {
  149. release_name: [
  150. 'release_name',
  151. {
  152. validateFirst: true,
  153. rules: [
  154. { required: true, message: this.$t('helm.text_28') },
  155. { min: 2, max: 24, message: this.$t('helm.text_38'), trigger: 'blur' },
  156. { validator: this.$validate('k8sName') },
  157. ],
  158. },
  159. ],
  160. version: [
  161. 'version',
  162. {
  163. rules: [
  164. { required: true, message: this.$t('helm.text_30'), trigger: 'blur' },
  165. ],
  166. },
  167. ],
  168. domain: [
  169. 'domain',
  170. {
  171. rules: [
  172. { validator: isRequired(), message: this.$t('rules.domain'), trigger: 'change' },
  173. ],
  174. },
  175. ],
  176. project: [
  177. 'project',
  178. {
  179. rules: [
  180. { validator: isRequired(), message: this.$t('rules.project'), trigger: 'change' },
  181. ],
  182. },
  183. ],
  184. cluster: [
  185. 'cluster',
  186. {
  187. initialValue: this.$store.state.common.k8s.cluster,
  188. rules: [
  189. { required: true, message: this.$t('helm.text_39'), trigger: 'blur' },
  190. ],
  191. },
  192. ],
  193. namespace: [
  194. 'namespace',
  195. {
  196. initialValue: this.$store.state.common.k8s.namespace,
  197. rules: [
  198. { required: true, message: this.$t('helm.text_40'), trigger: 'blur' },
  199. ],
  200. },
  201. ],
  202. config: {
  203. key: i => [
  204. `keys[${i}]`,
  205. {
  206. rules: [
  207. { required: true, message: this.$t('helm.text_41') },
  208. ],
  209. },
  210. ],
  211. value: i => [
  212. `values[${i}]`,
  213. {
  214. rules: [
  215. { required: true, message: this.$t('helm.text_42') },
  216. ],
  217. },
  218. ],
  219. },
  220. yaml: [
  221. 'yaml',
  222. {
  223. validateFirst: true,
  224. rules: [
  225. { required: true, message: this.$t('helm.text_43') },
  226. { validator },
  227. ],
  228. },
  229. ],
  230. },
  231. definition: [
  232. // 'hypervisor',
  233. // 'preferRegion',
  234. // 'preferZone',
  235. // 'network',
  236. // 'virtualMachines',
  237. // 'virtualMachines.masterNode',
  238. // 'virtualMachines.slaveNode',
  239. // 'virtualMachines.masterNode.instanceType',
  240. // 'virtualMachines.masterNode.diskSizeGB',
  241. // 'virtualMachines.masterNode.storageBackend',
  242. // 'virtualMachines.masterNode.ansiblePlaybook',
  243. // 'virtualMachines.masterNode.ansiblePlaybook.jenkins',
  244. // 'virtualMachines.masterNode.ansiblePlaybook.telegraf',
  245. // 'virtualMachines.masterNode.ansiblePlaybook.jenkins.adminUsername',
  246. // 'virtualMachines.masterNode.ansiblePlaybook.jenkins.adminPassword',
  247. // 'virtualMachines.masterNode.ansiblePlaybook.jenkins.httpPort',
  248. // 'virtualMachines.masterNode.ansiblePlaybook.telegraf.influxdbName',
  249. // 'virtualMachines.masterNode.ansiblePlaybook.telegraf.influxdbUrl',
  250. // 'virtualMachines.slaveNode.count',
  251. // 'virtualMachines.slaveNode.instanceType',
  252. // 'virtualMachines.slaveNode.diskSizeGB',
  253. // 'virtualMachines.slaveNode.storageBackend',
  254. ],
  255. }
  256. },
  257. computed: {
  258. compiledMarkdown () {
  259. if (!this.chartDetail.readme) return this.$t('helm.text_47')
  260. const markdownDoc = Base64.decode(this.chartDetail.readme)
  261. return marked(markdownDoc, { sanitize: true })
  262. },
  263. ...mapGetters(['isAdminMode', 'isProjectMode', 'scope', 'isDomainMode', 'userInfo', 'l3PermissionEnable']),
  264. },
  265. watch: {
  266. 'form.fd.domain.key' () {
  267. this.getCapability()
  268. },
  269. },
  270. created () {
  271. this.chartsM = new this.$Manager('charts', 'v1')
  272. this.releaseM = new this.$Manager('releases', 'v1')
  273. this.endpointM = new this.$Manager('endpoints', 'v1')
  274. this.fetchChartData()
  275. },
  276. methods: {
  277. async getCapability () {
  278. if (!this.definition || !this.definition.length) return
  279. const params = { project_domain: _.get(this.form.fd, 'domain.key') }
  280. try {
  281. const { data: { data } } = await new this.$Manager('capabilities', 'v2').list({ params })
  282. if (data && data.length) {
  283. const index = this.definition.findIndex(val => {
  284. if (R.is(Object, val)) {
  285. if (R.is(Array, val.key)) {
  286. return val.key[0] === 'hypervisor'
  287. }
  288. return val.key === 'hypervisor'
  289. }
  290. return val === 'hypervisor'
  291. })
  292. const { hypervisors = [] } = data[0]
  293. const hyperItem = this.definition[index]
  294. const hyperObj = R.is(Object, hyperItem) ? hyperItem : { key: hyperItem }
  295. const hypervisorOpts = hypervisors.filter(v => v !== 'baremetal').map(v => ({ value: v, label: (_.get(HYPERVISORS_MAP, `${v}.label`) || v) }))
  296. this.definition.splice(index, 1, {
  297. ...hyperObj,
  298. type: 'a-select',
  299. options: hypervisorOpts,
  300. })
  301. this.$refs.formRef.form.fc.setFieldsValue({
  302. hypervisor: hypervisorOpts[0] ? hypervisorOpts[0].value : undefined,
  303. })
  304. }
  305. } catch (error) {
  306. throw error
  307. }
  308. },
  309. getDefinition (jsonshcema = this.schema) {
  310. this.definition = Object.keys(jsonshcema.properties).filter(val => val !== 'project')
  311. this.getCapability()
  312. },
  313. valueSearch (query, path) {
  314. const values = this.chartDetail.chart.values
  315. const value = _.get(values, path)
  316. if (value && (R.is(String, value) || R.is(Number, value))) {
  317. const opts = []
  318. if (!query || (value === query || (query && value.toLowerCase().includes(query.toLowerCase())))) opts.push(value)
  319. return opts
  320. }
  321. return []
  322. },
  323. async validateForm () {
  324. try {
  325. const values = await this.$refs.formRef.handleSubmit()
  326. const validData = compactObj(values) // 去除 属性值是 undefined,''和null的
  327. return validData
  328. } catch (error) {
  329. throw error
  330. }
  331. },
  332. async getChart (version) {
  333. const { repo, name } = this.$route.query
  334. const { data } = await this.chartsM.get({
  335. id: name,
  336. params: {
  337. version: version,
  338. repo: repo,
  339. },
  340. })
  341. // const ret = await this.endpointM.list({
  342. // params: {
  343. // service: 'influxdb',
  344. // interface: 'public',
  345. // },
  346. // })
  347. // const ends = ret.data.data || []
  348. // if (ends.length) {
  349. // this.influxdbUrl = ends[0].url
  350. // }
  351. if (data) {
  352. this.chartDetail = data
  353. this.previewFiles = data.files
  354. if (this.activeKey.indexOf('yaml')) {
  355. this.activeKey.push('yaml')
  356. }
  357. const jsonSchemaItem = data.files.find(val => val.name.endsWith('values.schema.json'))
  358. const clearInflushdbInfo = (schema) => {
  359. const telegraf = schema?.properties?.virtualMachines?.properties?.masterNode?.properties?.ansiblePlaybook?.properties?.telegraf
  360. if (telegraf) {
  361. const influxdb = telegraf?.properties
  362. if (influxdb) {
  363. influxdb.influxdbName.default = ''
  364. influxdb.influxdbUrl.default = ''
  365. }
  366. }
  367. return schema
  368. }
  369. if (R.is(Object, jsonSchemaItem)) {
  370. let schema = Base64.decode(jsonSchemaItem.data)
  371. if (R.is(String, schema)) {
  372. schema = JSON.parse(schema)
  373. }
  374. this.appendImageToVm(schema)
  375. delete schema.properties.envs
  376. this.schema = clearInflushdbInfo(schema)
  377. this.getDefinition()
  378. this.isJsonSchema = true
  379. }
  380. }
  381. },
  382. appendImageToVm (schema) {
  383. if (schema.properties && schema.properties.virtualMachines && schema.properties.virtualMachines.properties) {
  384. // 多节点
  385. for (const key in schema.properties.virtualMachines.properties) {
  386. this.appendImageToNode(schema.properties.virtualMachines.properties[key], key)
  387. }
  388. } else if (schema.properties.virtualMachine) {
  389. // 单节点
  390. this.appendImageToNode(schema.properties.virtualMachine, '')
  391. }
  392. },
  393. appendImageToNode (node, nodeName) {
  394. if (node.required && node.required.includes('image') && node.properties && !node.properties.image) {
  395. node.properties.image = {
  396. title: '镜像',
  397. type: 'string',
  398. componentType: 'ImageList',
  399. description: nodeName === 'slaveNode' ? '' : '请选择镜像',
  400. }
  401. const temp = { ...node.properties }
  402. node.properties = {}
  403. node.required.map(key => {
  404. node.properties[key] = temp[key]
  405. delete temp[key]
  406. })
  407. node.properties = { ...node.properties, ...temp }
  408. }
  409. },
  410. async fetchChartData () {
  411. const version = await this.getChartVersion()
  412. this.getChart(version)
  413. },
  414. async getChartVersion () {
  415. this.versionLoading = true
  416. const { repo, name } = this.$route.query
  417. const { data: { data = [] } } = await this.chartsM.list({
  418. params: {
  419. repo,
  420. name,
  421. all_version: true,
  422. },
  423. })
  424. if (data.length === 0) {
  425. this.$message.error(this.$t('helm.text_44'))
  426. return
  427. }
  428. this.versions = data.map(v => {
  429. return {
  430. label: `${v.repo} - ${v.version}`,
  431. key: `${v.repo} - ${v.version}`,
  432. }
  433. })
  434. this.form.fc.getFieldDecorator(this.decorators.version[0], this.decorators.version[1])
  435. this.form.fc.setFieldsValue({
  436. [this.decorators.version[0]]: this.versions[0].key,
  437. })
  438. return data[0].version
  439. },
  440. async doCreate (values, valuesJson) {
  441. const data = {
  442. chart_name: `${this.chartDetail.repo}/${this.chartDetail.name}`,
  443. release_name: values.release_name,
  444. }
  445. if (this.isVm) {
  446. if (this.isAdminMode) {
  447. data.project = values.project.key
  448. if (this.l3PermissionEnable) {
  449. data.domain = values.domain.key
  450. }
  451. }
  452. if (this.isDomainMode) {
  453. data.project = values.project.key
  454. data.domain = this.userInfo.projectDomainId
  455. }
  456. if (this.isProjectMode) {
  457. data.domain = this.userInfo.projectDomainId
  458. data.project = this.userInfo.projectId
  459. }
  460. } else {
  461. data.cluster = values.cluster
  462. data.namespace = values.namespace
  463. }
  464. if (!R.isNil(values.version) && !R.isEmpty(values.version)) {
  465. data.version = values.version.split(' - ')[1]
  466. }
  467. if (valuesJson) {
  468. data.values_json = valuesJson
  469. data.values_json.project = data.project
  470. data.values_json.envs = {
  471. project_id: data.project,
  472. domain_id: data.domain || 'default', // 兼容没开启三级权限情况
  473. }
  474. } else {
  475. const sets = {}
  476. if (this.formActiveTab === 'form') {
  477. R.forEachObjIndexed((value, key) => {
  478. sets[value] = values.values[key]
  479. }, values.keys)
  480. data.sets = sets
  481. } else {
  482. data.values = values.yaml
  483. }
  484. }
  485. await this.releaseM.create({ data })
  486. },
  487. async confirm () {
  488. this.loading = true
  489. const jobs = [this.form.fc.validateFields()]
  490. if (this.isJsonSchema) {
  491. jobs.push(this.validateForm())
  492. }
  493. try {
  494. const [values, valuesJson] = await Promise.all(jobs)
  495. await this.doCreate(values, valuesJson)
  496. this.$message.success(this.$t('helm.text_45'))
  497. this.loading = false
  498. this.cancel(true)
  499. } catch (error) {
  500. this.loading = false
  501. throw error
  502. }
  503. },
  504. cancel (isSuccess) {
  505. if (isSuccess) {
  506. if (this.isVm) {
  507. this.$router.push('/vm-release')
  508. } else {
  509. this.$router.push('/k8s-release')
  510. }
  511. } else {
  512. this.$router.push('/k8s-chart')
  513. }
  514. },
  515. },
  516. }
  517. </script>