diff --git a/locales/zh/l10n-devopsProjects-continuousDeployments-list.js b/locales/zh/l10n-devopsProjects-continuousDeployments-list.js index 5e6a4fd7582..72ef7bbf7c4 100644 --- a/locales/zh/l10n-devopsProjects-continuousDeployments-list.js +++ b/locales/zh/l10n-devopsProjects-continuousDeployments-list.js @@ -18,6 +18,7 @@ module.exports = { // Banner CONTINUOUS_DEPLOYMENT_PL: '持续部署', + CONTINUOUS_DEPLOYMENT_DESC: '管理持续部署,以通过 GitOps 持续部署资源。 ', // List CONTINUOUS_DEPLOYMENT_EMPTY_DESC: '请创建一个部署。', diff --git a/locales/zh/l10n-projects-imageBuilders-list.js b/locales/zh/l10n-projects-imageBuilders-list.js index 1fc77036d2c..2ca3807a61e 100644 --- a/locales/zh/l10n-projects-imageBuilders-list.js +++ b/locales/zh/l10n-projects-imageBuilders-list.js @@ -19,6 +19,7 @@ module.exports = { // Banner IMAGE_BUILDER_PL: '镜像构建器', IMAGE_BUILDER_DESC: '镜像构建器(Image Builder)是将代码或者制品制作成容器镜像的工具。您可以通过简单的设置将制品或代码直接制作成容器镜像。', + // List IMAGE_BUILDER_EMPTY_DESC: '请创建一个镜像构建器。', NOT_RUNNING_YET: '未运行', diff --git a/server/config.yaml b/server/config.yaml index 6a20f0bade7..ee4880c46b2 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -412,6 +412,12 @@ client: icon: vnas, clusterModule: devops, } + - { + name: new_s2ibuilders, + title: NEW_IMAGE_BUILDER_PL, + icon: vnas, + clusterModule: devops, + } - name: monitoring title: MONITORING_AND_ALERTING icon: monitor @@ -515,6 +521,14 @@ client: authKey: "applications", requiredClusterVersion: v3.3.0, } + # 新s2i + - { + name: image-builder, + title: IMAGE_BUILDER_PL, + icon: rocket, + authKey: "applications", + requiredClusterVersion: v3.3.0, + } - { name: code-repo, title: CODE_REPO_PL, diff --git a/src/actions/imagebuilder.js b/src/actions/imagebuilder.js index 0ebe13a3d58..74cdde262c9 100644 --- a/src/actions/imagebuilder.js +++ b/src/actions/imagebuilder.js @@ -36,13 +36,16 @@ const filterImageEnv = (data, fn) => { } export default { + //当触发 'imagebuilder.create' 操作时,会打开一个模态框(弹窗),显示一个创建图像构建的表单。 'imagebuilder.create': { on({ store, cluster, namespace, module, success, ...props }) { + //获取 formTemplate:根据模块(module)获取表单模板。 const formTemplate = FORM_TEMPLATES[module]({ namespace, }) - + //通过 Modal.open 打开模态框,传入一系列属性设置 const modal = Modal.open({ + //用户点击模态框中的确认按钮后执行的操作 onOk: data => { if (!data) { return @@ -50,7 +53,7 @@ export default { const environment = filterImageEnv(data, v => !isEmpty(v)) set(data, 'spec.config.environment', environment) - + //确认按钮点击后,关闭,显示成功,清除表单数据 store.create(data, { cluster, namespace }).then(() => { Modal.close(modal) Notify.success({ content: t('CREATE_SUCCESSFUL') }) @@ -71,7 +74,9 @@ export default { }) }, }, + //当触发 'imagebuilder.rerun' 操作时 'imagebuilder.rerun': { + //打开一个模态框,显示一个重新运行图像构建的表单 on({ store, detail, success, ...props }) { const modal = Modal.open({ onOk: data => { diff --git a/src/components/HOCs/withList.js b/src/components/HOCs/withList.js index ead448ba704..00fb4c05306 100644 --- a/src/components/HOCs/withList.js +++ b/src/components/HOCs/withList.js @@ -26,7 +26,9 @@ import { MODULE_KIND_MAP } from 'utils/constants' import { trigger } from 'utils/action' import ObjectMapper from 'utils/object.mapper' +//用于生列表展示相关功能的工厂函数,接收options,返回一个函数WrappedComponent export default function withList(options) { + //该返回函数接受一个目标组件,返回增强的组件listwrapper return WrappedComponent => { const ObserverComponent = observer(WrappedComponent) class ListWrapper extends React.Component { diff --git a/src/pages/devops/containers/ImageBuilder/Detail/BuildRecords/index.jsx b/src/pages/devops/containers/ImageBuilder/Detail/BuildRecords/index.jsx new file mode 100644 index 00000000000..7a56298b431 --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/BuildRecords/index.jsx @@ -0,0 +1,61 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import React from 'react' +import { get } from 'lodash' +import { observer, inject } from 'mobx-react' + +import ImageBuilderLastRun from 'projects/components/Cards/ImageBuilderLastRun' +import RunRecords from 'projects/components/Cards/ImageRunRecord' +import styles from './index.scss' + +@inject('detailStore', 's2iRunStore') +@observer +class BuildRecords extends React.Component { + get store() { + return this.props.detailStore + } + + get isB2i() { + return get(this.store.detail, 'spec.config.isBinaryURL') + } + + render() { + const { params } = this.props.match + const { runDetail } = this.props.s2iRunStore + + return ( + +
{t('LAST_BUILD_ENVIRONMENT')}
+
+ +
+
{t('RUN_RECORDS')}
+
+ +
+
+ ) + } +} + +export default BuildRecords diff --git a/src/pages/devops/containers/ImageBuilder/Detail/BuildRecords/index.scss b/src/pages/devops/containers/ImageBuilder/Detail/BuildRecords/index.scss new file mode 100644 index 00000000000..d44177848be --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/BuildRecords/index.scss @@ -0,0 +1,43 @@ +@import '~scss/variables'; +@import '~scss/mixins'; + +.main { + padding: 0; + + > div:first-child { + padding: 20px 20px 0 20px; + } +} + +.table { + :global { + .table { + border-radius: 0 0 $border-radius $border-radius; + } + } +} + +.clickable { + &:hover { + cursor: pointer; + color: $btn-primary-hover-bg; + } +} + +.content { + padding-top: 20px; +} + +.title { + height: 20px; + margin-bottom: 5px; + @include TypographySymbolText; +} + +.card { + padding: 12px; + margin-bottom: 12px; + border-radius: 4px; + box-shadow: 0 4px 8px 0 rgba(36, 46, 66, 0.06); + background-color: #ffffff; +} \ No newline at end of file diff --git a/src/pages/devops/containers/ImageBuilder/Detail/Environment/index.jsx b/src/pages/devops/containers/ImageBuilder/Detail/Environment/index.jsx new file mode 100644 index 00000000000..0233da8b69a --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/Environment/index.jsx @@ -0,0 +1,95 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import React from 'react' +import { toJS } from 'mobx' +import { observer, inject } from 'mobx-react' +import { isEmpty, get } from 'lodash' + +import EnvStore from 'stores/workload/env' + +import ContainerEnvCard from 'components/Cards/Containers/EnvVariables' + +@inject('s2iRunStore') +@observer +export default class EnvVariables extends React.Component { + constructor(props) { + super(props) + this.envStore = new EnvStore() + } + + componentDidMount() { + this.fetchData() + } + + get module() { + return this.props.module + } + + get store() { + return this.props.s2iRunStore + } + + get namespace() { + return this.store.jobDetail.namespace + } + + get cluster() { + return this.props.match.params.cluster + } + + get containers() { + const data = toJS(this.store.jobDetail) + const { spec, containers = [] } = data + + if (this.module === 'containers') return [data] + + if (!isEmpty(containers)) return containers + if (!isEmpty(spec)) return get(spec, 'template.spec.containers', []) + + return [] + } + + get test() { + return '2' + } + + fetchData = () => { + this.envStore.fetchList({ + cluster: this.cluster, + namespace: this.namespace, + containers: this.containers, + }) + } + + render() { + const { data, isLoading } = toJS(this.envStore.list) + return ( +
+ {data.map((container, index) => ( + + ))} +
+ ) + } +} diff --git a/src/pages/devops/containers/ImageBuilder/Detail/Events/index.jsx b/src/pages/devops/containers/ImageBuilder/Detail/Events/index.jsx new file mode 100644 index 00000000000..c6efc62a9e7 --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/Events/index.jsx @@ -0,0 +1,68 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import React from 'react' +import { toJS } from 'mobx' +import { observer, inject } from 'mobx-react' +import { joinSelector } from 'utils' +import EventStore from 'stores/event' + +import EventsCard from 'components/Cards/Events' + +class Events extends React.Component { + constructor(props) { + super(props) + + this.eventStore = new EventStore() + this.fetchData() + } + + get store() { + return this.props.s2iRunStore + } + + get namespace() { + return this.store.jobDetail.namespace + } + + fetchData = () => { + const { uid, name, namespace } = this.store.jobDetail + const { cluster } = this.props.match.params + const fields = { + 'involvedObject.name': name, + 'involvedObject.namespace': namespace, + 'involvedObject.kind': 'Job', + 'involvedObject.uid': uid, + } + + this.eventStore.fetchList({ + cluster, + namespace: this.namespace, + fieldSelector: joinSelector(fields), + }) + } + + render() { + const { data, isLoading } = toJS(this.eventStore.list) + + return + } +} + +export default inject('s2iRunStore')(observer(Events)) +export const Component = Events diff --git a/src/pages/devops/containers/ImageBuilder/Detail/ImageProduct/index.jsx b/src/pages/devops/containers/ImageBuilder/Detail/ImageProduct/index.jsx new file mode 100644 index 00000000000..034fc672f13 --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/ImageProduct/index.jsx @@ -0,0 +1,39 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import React from 'react' +import { observer } from 'mobx-react' + +import { Card } from 'components/Base' +import ImageArtifactsCard from 'projects/components/Cards/ImageArtifacts' +import styles from './index.scss' + +@observer +class ImageArtifacts extends React.Component { + render() { + const { params } = this.props.match + + return ( + + + + ) + } +} + +export default ImageArtifacts diff --git a/src/pages/devops/containers/ImageBuilder/Detail/ImageProduct/index.scss b/src/pages/devops/containers/ImageBuilder/Detail/ImageProduct/index.scss new file mode 100644 index 00000000000..b0595c66a50 --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/ImageProduct/index.scss @@ -0,0 +1,3 @@ +.table { + box-shadow: none; +} \ No newline at end of file diff --git a/src/pages/devops/containers/ImageBuilder/Detail/ResourceStatus/index.jsx b/src/pages/devops/containers/ImageBuilder/Detail/ResourceStatus/index.jsx new file mode 100644 index 00000000000..b90c773fb4c --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/ResourceStatus/index.jsx @@ -0,0 +1,58 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import React from 'react' +import { observer, inject } from 'mobx-react' +import { Component as Base } from 'projects/containers/Deployments/Detail/ResourceStatus' +import PodsCard from 'components/Cards/Pods' +import { Loading } from '@kube-design/components' + +@inject('detailStore', 's2iRunStore') +@observer +class JobsResourceStatus extends Base { + get store() { + return this.props.s2iRunStore + } + + renderPods() { + const { workspace, cluster } = this.props.match.params + + return ( + + ) + } + + renderContent() { + const { isLoading } = this.store + + if (isLoading) { + return + } + return ( +
+ {this.renderContainerPorts()} + {this.renderPods()} +
+ ) + } +} + +export default JobsResourceStatus diff --git a/src/pages/devops/containers/ImageBuilder/Detail/index.jsx b/src/pages/devops/containers/ImageBuilder/Detail/index.jsx new file mode 100644 index 00000000000..8425a723842 --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/index.jsx @@ -0,0 +1,254 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import React from 'react' +import { toJS } from 'mobx' +import { observer, inject } from 'mobx-react' +import { get, isArray } from 'lodash' + +import { Loading, Notify, Icon } from '@kube-design/components' + +import { getDisplayName, getLocalTime, parseUrl } from 'utils' +import { trigger } from 'utils/action' +import S2IBuilderStore from 'stores/s2i/builder' +import S2IRunStore from 'stores/s2i/run' +import ResourceStore from 'stores/workload/resource' + +import DetailPage from 'projects/containers/Base/Detail' + +import { CopyToClipboard } from 'react-copy-to-clipboard' +import getRoutes from './routes' + +@inject('rootStore') +@observer +@trigger +export default class ImageBuilderDetail extends React.Component { + store = new S2IBuilderStore(this.module) + + resourceStore = new ResourceStore(this.module) + + s2iRunStore = new S2IRunStore() + + componentDidMount() { + this.fetchData() + } + + get module() { + return 's2ibuilders' + } + + get name() { + return 'IMAGE_BUILDER' + } + + get routing() { + return this.props.rootStore.routing + } + + get params() { + return this.props.match.params + } + + get listUrl() { + const { workspace, cluster, namespace } = this.props.match.params + return `${ + workspace ? `/${workspace}` : '' + }/clusters/${cluster}/projects/${namespace}/${this.module}` + } + //异步获取image构建和相关数据的详细信息 + fetchData = async params => { + const builderResult = await this.store.fetchDetail(this.params, params) + const runDetail = await this.s2iRunStore.fetchRunDetail({ + ...this.params, + runName: get(builderResult, 'status.lastRunName', ''), + }) + await this.s2iRunStore.fetchJobDetail({ + ...this.params, + name: get(runDetail, '_originData.status.kubernetesJobName', ''), + }) + } + + handleCopy = () => { + Notify.success({ + content: t('COPIED_SUCCESSFUL'), + }) + } + + getOperations = () => [ + { + key: 'Run', + text: t('RUN'), + action: 'edit', + type: 'control', + onClick: () => + this.trigger('imagebuilder.rerun', { + detail: toJS(this.store.detail), + success: this.fetchData, + }), + }, + { + key: 'edit', + icon: 'pen', + text: t('EDIT_INFORMATION'), + action: 'edit', + onClick: () => + this.trigger('resource.baseinfo.edit', { + type: this.name, + detail: toJS(this.store.detail), + success: this.fetchData, + }), + }, + { + key: 'editYaml', + icon: 'pen', + text: t('EDIT_YAML'), + action: 'edit', + onClick: () => + this.trigger('resource.yaml.edit', { + detail: this.store.detail, + success: this.fetchData, + }), + }, + { + key: 'delete', + icon: 'trash', + text: t('DELETE'), + action: 'delete', + type: 'danger', + onClick: () => + this.trigger('resource.delete', { + type: this.name, + detail: this.store.detail, + success: () => this.routing.push(this.listUrl), + }), + }, + ] + + pathAddCluster = (path, cluster) => { + const match = path.match(/(\/kapis|api|apis)(.*)/) + return !cluster || cluster === 'default' || !isArray(match) + ? path + : `${match[1]}/clusters/${cluster}${match[2]}` + } + + getAttrs = () => { + const detail = toJS(this.store.detail) + const { cluster } = this.props.match.params + const { spec = {} } = detail + const isBinaryURL = get(spec, 'config.isBinaryURL', '') + const { binaryName } = this.s2iRunStore.runDetail + const sourceUrl = get(spec, 'config.sourceUrl', '') + const path = get(parseUrl(sourceUrl), 'pathname', `/${sourceUrl}`) + const url = this.pathAddCluster(path, cluster) + const downLoadUrl = `${window.location.protocol}//${window.location.host}/b2i_download${url}` + const secret = get(detail, 'spec.config.secretCode', '') + const triggerLink = `http://s2ioperator-trigger-service.kubesphere-devops-system.svc/s2itrigger/v1alpha1/general/namespaces/${ + detail.namespace + }/s2ibuilders/${detail.name}/${secret ? `?secretCode=${secret}` : ''}` + + return [ + { + name: t('NAME'), + value: detail.name, + }, + { + name: t('PROJECT'), + value: detail.namespace, + }, + { + name: t('TYPE'), + value: t(detail.type), + }, + { + name: t('BUILDER_IMAGE'), + value: get(spec, 'config.builderImage', '-'), + }, + { + name: t('IMAGE_NAME'), + value: get(spec, 'config.imageName', '-'), + }, + { + name: t('PULL_POLICY'), + value: get(spec, 'config.builderPullPolicy', '-'), + }, + { + name: t('CODE_REPOSITORY_URL'), + value: isBinaryURL ? ( + + {binaryName} + + ) : ( + sourceUrl + ), + }, + { + name: t('REMOTE_TRIGGER'), + value: ( + <> + {triggerLink} + + + + + ), + }, + { + name: t('CREATION_TIME_TCAP'), + value: getLocalTime(detail.createTime).format('YYYY-MM-DD HH:mm:ss'), + }, + { + name: t('CREATOR'), + value: detail.creator, + }, + ] + } + + render() { + const stores = { + detailStore: this.store, + s2iRunStore: this.s2iRunStore, + resourceStore: this.resourceStore, + } + + if (this.store.isLoading || this.s2iRunStore.isLoading) { + return + } + + const sideProps = { + module: this.module, + name: getDisplayName(this.store.detail), + desc: this.store.detail.description, + operations: this.getOperations(), + attrs: this.getAttrs(), + breadcrumbs: [ + { + label: t('IMAGE_BUILDER_PL'), + url: this.listUrl, + }, + ], + } + + return ( + + ) + } +} diff --git a/src/pages/devops/containers/ImageBuilder/Detail/index.scss b/src/pages/devops/containers/ImageBuilder/Detail/index.scss new file mode 100644 index 00000000000..580f5ec5293 --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/index.scss @@ -0,0 +1,11 @@ +@import '~scss/variables'; + +.loading { + text-align: center; + padding: 30px 0; +} + +.modalInfo { + margin-bottom: 20px; +} + diff --git a/src/pages/devops/containers/ImageBuilder/Detail/routes.js b/src/pages/devops/containers/ImageBuilder/Detail/routes.js new file mode 100644 index 00000000000..07930989a50 --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/Detail/routes.js @@ -0,0 +1,54 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import { getIndexRoute } from 'utils/router.config' + +import ImageBuildRecords from './BuildRecords' +import ResourceStatus from './ResourceStatus' +import Env from './Environment' +import Events from './Events' +import ImageArtifacts from './ImageProduct' + +export default path => [ + { + path: `${path}/records`, + title: 'RUN_RECORDS', + component: ImageBuildRecords, + excat: true, + }, + { + path: `${path}/resource-status`, + title: 'RESOURCE_STATUS', + component: ResourceStatus, + excat: true, + }, + { + path: `${path}/image-artifacts`, + title: 'IMAGE_ARTIFACTS', + component: ImageArtifacts, + excat: true, + }, + { + path: `${path}/env`, + title: 'ENVIRONMENT_VARIABLE_PL', + component: Env, + excat: true, + }, + { path: `${path}/events`, title: 'EVENT_PL', component: Events, excat: true }, + getIndexRoute({ path, to: `${path}/records`, exact: true }), +] diff --git a/src/pages/devops/containers/ImageBuilder/index.jsx b/src/pages/devops/containers/ImageBuilder/index.jsx new file mode 100644 index 00000000000..5af90894359 --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/index.jsx @@ -0,0 +1,210 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import React from 'react' +import { reaction } from 'mobx' +import { Link } from 'react-router-dom' +import { get } from 'lodash' + +import { Avatar, Status } from 'components/Base' +import Banner from 'components/Cards/Banner' +import withList, { ListPage } from 'components/HOCs/withList' +import Table from 'components/Tables/List' + +import { getLocalTime, getDisplayName } from 'utils' +import { ICON_TYPES } from 'utils/constants' + +import S2IBuilderStore from 'stores/s2i/builder' +import { inject, observer, Provider } from 'mobx-react' +@inject('devopsStore') +@withList({ + store: new S2IBuilderStore('s2ibuilders'), + module: 's2ibuilders', + name: 'IMAGE_BUILDER', +}) +export default class ImageBuilders extends React.Component { + get store() { + return this.props.store + } + // 组件挂载后的操作,创建一个MobX反应(reacttion), + //在WrappedComponent变化的时候进行一些操作,比如设定设定定时器刷新列表等 + componentDidMount() { + this.freshDisposer = reaction( + () => this.store.list.isLoading, + () => { + const isRunning = this.store.list.data.some( + detail => get(detail, 'status.lastRunState', 'Running') === 'Running' + ) + clearTimeout(this.freshTimer) + if (isRunning) { + this.freshTimer = setTimeout(this.handleFresh, 4000) + } + }, + { fireImmediately: true } + ) + } + //组件卸载钱,清理之前创建的定时器 + componentWillUnmount() { + this.freshDisposer && this.freshDisposer() + clearTimeout(this.freshTimer) + } + // 定义了一个数组,包含用于列表项操作的配置信息,比如编辑和删除 + get itemActions() { + const { trigger, name } = this.props + return [ + { + key: 'edit', + icon: 'pen', + text: t('EDIT_INFORMATION'), + action: 'edit', + onClick: item => + trigger('resource.baseinfo.edit', { + detail: item, + }), + }, + { + key: 'delete', + icon: 'trash', + text: t('DELETE'), + action: 'delete', + onClick: item => + trigger('resource.delete', { + type: name, + detail: item, + }), + }, + ] + } + //定义了一个函数,返回展示在列表中的列配置,包括名称、状态、类型、服务、创建时间等 + getColumns = () => { + const { prefix, module } = this.props + return [ + //列表名称栏 + { + title: t('NAME'), + dataIndex: 'name', + render: (name, record) => ( + + ), + }, + //状态 + { + title: t('STATUS'), + dataIndex: 'status', + isHideable: true, + width: '15%', + render: status => { + let _status = get(status, 'lastRunState', '') + _status = _status === 'Running' ? 'Building' : _status + return ( + + ) + }, + }, + //类型 + { + title: t('TYPE'), + dataIndex: 'type', + isHideable: true, + width: '15%', + render: type => type && t(type.toUpperCase()), + }, + //服务 + { + title: t('SERVICE'), + dataIndex: 'serviceName', + isHideable: true, + width: '15%', + render: name => { + if (name) { + return {name} + } + return '-' + }, + }, + //创建时间 + { + title: t('CREATION_TIME_TCAP'), + dataIndex: 'createTime', + isHideable: true, + width: 150, + render: time => getLocalTime(time).format('YYYY-MM-DD HH:mm:ss'), + }, + ] + } + //定义函数用于出发创建s2i构建操作 + showCreate = () => { + // gbk + // const { match, module, projectStore } = this.props + const { match, module, devopsStore } = this.props + //trigger用于出发js中定义的函数 + return this.props.trigger('imagebuilder.create', { + module, + projectDetail: devopsStore.detail, + // projectDetail: projectStore.detail, + namespace: match.params.devops, + // namespace: match.params.namespace, + cluster: match.params.cluster, + }) + } + // gbk + getData = async ({ silent, ...params } = {}) => { + const store = this.props.store + + silent && (store.list.silent = true) + await store.fetchList({ + ...this.props.match.params, + ...params, + namespace: this.props.match.params.devops, + }) + store.list.silent = false + } + + //渲染页面内容,包括 Banner、Table 和列表展示相关的组件,以及相关的 props + render() { + const { bannerProps, tableProps } = this.props + return ( + // gbk + + + + + ) + } +} diff --git a/src/pages/devops/containers/ImageBuilder/index.scss b/src/pages/devops/containers/ImageBuilder/index.scss new file mode 100644 index 00000000000..826e05cf4eb --- /dev/null +++ b/src/pages/devops/containers/ImageBuilder/index.scss @@ -0,0 +1,76 @@ +@import '~scss/variables'; + +.header { + height: 100px; + margin-bottom: 12px; + padding: 24px 20px; + border-radius: 4px; + background-color: #ffffff; + &:hover { + box-shadow: 0 4px 8px 0 rgba(223, 226, 233, 0.06); + } + + h3 { + height: 32px; + text-shadow: 0 2px 4px rgba(36, 46, 66, 0.1); + font-family: $font-family-id; + font-size: 24px; + line-height: 1.33; + font-weight: 600; + color: $dark-color07; + } + + p { + height: 20px; + font-family: $font-family-id; + font-size: 12px; + line-height: 1.67; + color: $dark-color01; + } + + .icon { + float: left; + margin-right: 20px; + width: 52px; + height: 52px; + border-radius: 100px; + border-top-right-radius: 0px; + background-color: $light-color02; + + svg { + width: 40px; + height: 40px; + } + } +} + +.status { + :global .icon { + margin-left: 4px; + } +} + +.tabs { + cursor: pointer; + padding: 2px; + border-radius: 18px; + display: inline-block; + background-color: $bg-color; + border: 1px solid #d8dee5; + margin-right: 20px; + + span { + display: inline-block; + text-align: center; + min-width: 120px; + line-height: 32px; + color: #79879c; + padding: 0 30px; + } +} + +.tabs__selected { + border-radius: 16px; + background-color: #36435c; + color: white !important; +} diff --git a/src/pages/devops/routes/index.js b/src/pages/devops/routes/index.js index 7f04825070b..e170512dc5d 100644 --- a/src/pages/devops/routes/index.js +++ b/src/pages/devops/routes/index.js @@ -23,7 +23,7 @@ import DevopsListLayout from '../containers/Base/List' import PipelinesList from '../containers/Pipelines/PipelinesList' import CDList from '../containers/CD/CDList' import CRList from '../containers/CodeRepo/CRList' - +import ImageBuilder from '../containers/ImageBuilder' import BaseInfo from '../containers/BaseInfo' import Roles from '../containers/Roles' import Members from '../containers/Members' @@ -46,6 +46,8 @@ export default [ { path: `${PATH}/pipelines`, component: PipelinesList, exact: true }, { path: `${PATH}/cd`, component: CDList, exact: true }, { path: `${PATH}/code-repo`, component: CRList, exact: true }, + // devpos新增s2i + { path: `${PATH}/image-builder`, component: ImageBuilder, exact: true }, { path: `${PATH}/base-info`, component: BaseInfo, exact: true }, { path: `${PATH}/roles`, component: Roles, exact: true }, { path: `${PATH}/members`, component: Members, exact: true }, diff --git a/src/pages/projects/containers/ImageBuilder/Detail/index.jsx b/src/pages/projects/containers/ImageBuilder/Detail/index.jsx index 77a514c1834..8425a723842 100644 --- a/src/pages/projects/containers/ImageBuilder/Detail/index.jsx +++ b/src/pages/projects/containers/ImageBuilder/Detail/index.jsx @@ -70,7 +70,7 @@ export default class ImageBuilderDetail extends React.Component { workspace ? `/${workspace}` : '' }/clusters/${cluster}/projects/${namespace}/${this.module}` } - + //异步获取image构建和相关数据的详细信息 fetchData = async params => { const builderResult = await this.store.fetchDetail(this.params, params) const runDetail = await this.s2iRunStore.fetchRunDetail({ diff --git a/src/pages/projects/containers/ImageBuilder/index.jsx b/src/pages/projects/containers/ImageBuilder/index.jsx index ed12dd1afdd..0d37b99254b 100644 --- a/src/pages/projects/containers/ImageBuilder/index.jsx +++ b/src/pages/projects/containers/ImageBuilder/index.jsx @@ -40,7 +40,8 @@ export default class ImageBuilders extends React.Component { get store() { return this.props.store } - +// 组件挂载后的操作,创建一个MobX反应(reacttion), +//在WrappedComponent变化的时候进行一些操作,比如设定设定定时器刷新列表等 componentDidMount() { this.freshDisposer = reaction( () => this.store.list.isLoading, @@ -56,12 +57,12 @@ export default class ImageBuilders extends React.Component { { fireImmediately: true } ) } - +//组件卸载钱,清理之前创建的定时器 componentWillUnmount() { this.freshDisposer && this.freshDisposer() clearTimeout(this.freshTimer) } - +// 定义了一个数组,包含用于列表项操作的配置信息,比如编辑和删除 get itemActions() { const { trigger, name } = this.props return [ @@ -88,10 +89,11 @@ export default class ImageBuilders extends React.Component { }, ] } - +//定义了一个函数,返回展示在列表中的列配置,包括名称、状态、类型、服务、创建时间等 getColumns = () => { const { prefix, module } = this.props return [ + //列表名称栏 { title: t('NAME'), dataIndex: 'name', @@ -111,6 +113,7 @@ export default class ImageBuilders extends React.Component { /> ), }, + //状态 { title: t('STATUS'), dataIndex: 'status', @@ -128,6 +131,7 @@ export default class ImageBuilders extends React.Component { ) }, }, + //类型 { title: t('TYPE'), dataIndex: 'type', @@ -135,6 +139,7 @@ export default class ImageBuilders extends React.Component { width: '15%', render: type => type && t(type.toUpperCase()), }, + //服务 { title: t('SERVICE'), dataIndex: 'serviceName', @@ -147,6 +152,7 @@ export default class ImageBuilders extends React.Component { return '-' }, }, + //创建时间 { title: t('CREATION_TIME_TCAP'), dataIndex: 'createTime', @@ -156,9 +162,10 @@ export default class ImageBuilders extends React.Component { }, ] } - +//定义函数用于出发创建s2i构建操作 showCreate = () => { const { match, module, projectStore } = this.props + //trigger用于出发js中定义的函数 return this.props.trigger('imagebuilder.create', { module, projectDetail: projectStore.detail, @@ -166,7 +173,7 @@ export default class ImageBuilders extends React.Component { cluster: match.params.cluster, }) } - +//渲染页面内容,包括 Banner、Table 和列表展示相关的组件,以及相关的 props render() { const { bannerProps, tableProps } = this.props return ( diff --git a/src/stores/devops/builder.js b/src/stores/devops/builder.js new file mode 100644 index 00000000000..c83faa6be54 --- /dev/null +++ b/src/stores/devops/builder.js @@ -0,0 +1,256 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import { get, set, unset, uniq } from 'lodash' +import { action, observable } from 'mobx' +import Base from 'stores/base' +import { generateId } from 'utils' +import TEMPLATE from 'utils/form.templates' +import { S2I_SUPPORTED_TYPES, B2I_SUPPORTED_TYPES } from 'utils/constants' + +import S2IRunStore from './run' + +export default class S2IBuilderStore extends Base { + get apiVersion() { + return 'apis/devops.kubesphere.io/v1alpha1' + } + + module = 's2ibuilders' + + runStore = new S2IRunStore() + + @observable detail = {} + + getS2iSupportLanguage = async params => { + //调用函数获取模板 + const result = await this.getBuilderTemplate(params) + //根据模板信息计算支持的 S2I 语言类型 + const supportS2iLanguage = { + s2i: [...S2I_SUPPORTED_TYPES], + b2i: [...B2I_SUPPORTED_TYPES], + } + const b2iMap = { + java: 'jar', + tomcat: 'war', + } + if (result && result.items) { + result.items.forEach(template => { + if ( + get(template, 'metadata.labels["builder-type.kubesphere.io/b2i"]') + ) { + supportS2iLanguage.b2i.push( + b2iMap[template.spec.codeFramework] || template.spec.codeFramework + ) + } + if ( + get(template, 'metadata.labels["builder-type.kubesphere.io/s2i"]') + ) { + supportS2iLanguage.s2i.push(template.spec.codeFramework) + } + }) + } + supportS2iLanguage.s2i = uniq(supportS2iLanguage.s2i) + supportS2iLanguage.b2i = uniq(supportS2iLanguage.b2i) + return supportS2iLanguage + } + + //获取Builder模板函数 + getBuilderTemplate = async params => + await request.get( + `apis/devops.kubesphere.io/v1alpha1${this.getPath( + params + )}/s2ibuildertemplates` + ) + + // get方法获取detail + @action + fetchDetail = async ({ cluster, namespace, name }) => { + this.isLoading = true + const result = await request.get( + `apis/devops.kubesphere.io/v1alpha1${this.getPath({ + cluster, + namespace, + })}/s2ibuilders/${name}`, + undefined, + undefined, + error => { + if (error.reason === 'NotFound') { + return error + } + Promise.reject(error) + } + ) + this.detail = this.mapper(result) + this.detail.cluster = cluster + this.isLoading = false + return this.detail + } + + // 更新创建数据,对数据进行修改和处理,如生成名称、更新镜像名等操作 + updateCreateData(data, isEditor = false) { + if (!isEditor) { + const _name = `${get(data, 'spec.config.imageName', '').replace( + /[_/:]/g, + '-' + )}-${get(data, 'spec.config.tag')}` + data.metadata.name = `${_name.slice(0, 60)}-${generateId(3)}` + } + + let repoUrl = get(data, 'metadata.annotations["kubesphere.io/repoUrl"]', '') + repoUrl = repoUrl.replace(/^(http(s)?:\/\/)?(.*)$/, '$3') + + const imageName = get(data, 'spec.config.imageName', '') + + if (repoUrl && !imageName.startsWith(repoUrl)) { + const totalImageName = repoUrl.endsWith('/') + ? `${repoUrl}${imageName}` + : `${repoUrl}/${imageName}` + set(data, 'spec.config.imageName', totalImageName) + } + + if (data.isUpdateWorkload === false) { + set( + data, + 'metadata.annotations["devops.kubesphere.io/donotautoscale"]', + 'true' + ) + } else { + unset(data, 'metadata.annotations["devops.kubesphere.io/donotautoscale"]') + } + + delete data.isUpdateWorkload + } + + creatBinary(name, namespace, cluster) { + const data = TEMPLATE['b2iBuilders']({ name, namespace }) + return request.post( + `apis/devops.kubesphere.io/v1alpha1/${this.getPath({ + namespace, + cluster, + })}/s2ibinaries/${name}`, + data + ) + } + + //创建 S2I 构建,首先调用 updateCreateData 更新数据,然后调用request.post 发送 POST 请求,调用createS2IRun: 创建 S2I 运行实例。 + create(data, { cluster, namespace }) { + this.updateCreateData(data, false) + return request + .post(this.getListUrl({ cluster, namespace }), data) + .then(() => { + const binaryUrl = get(data, 'metadata.annotations.sourceUrl') + this.createS2IRun({ + cluster, + namespace, + builderName: get(data, 'metadata.name'), + binaryUrl, + }) + }) + } + + updateBuilder(data, { cluster, namespace, name }) { + this.updateCreateData(data, true) + return request + .patch(this.getDetailUrl({ cluster, namespace, name }), data) + .then(() => { + const binaryUrl = get(data, 'metadata.annotations.sourceUrl') + this.createS2IRun({ + cluster, + namespace, + builderName: get(data, 'metadata.name'), + binaryUrl, + }) + }) + } + + createS2IRun({ cluster, namespace, builderName, binaryUrl }) { + if (!builderName || !namespace) { + return + } + + const name = `${builderName.slice(0, 37)}-${generateId(3)}` + const data = { + apiVersion: 'devops.kubesphere.io/v1alpha1', + kind: 'S2iRun', + metadata: { + name, + cluster, + namespace, + }, + spec: { + builderName, + binaryUrl, + ...(binaryUrl ? { isBinaryURL: true } : {}), + }, + } + return this.runStore.create(data, { cluster, namespace }) + } + + @action + rerun = async ({ + name, + cluster, + namespace, + isUpdateWorkload = true, + ...rest + }) => { + const annotations = isUpdateWorkload + ? {} + : { + 'devops.kubesphere.io/donotautoscale': 'true', + } + return this.runStore.create({ + apiVersion: 'devops.kubesphere.io/v1alpha1', + kind: 'S2iRun', + metadata: { + annotations, + name: `${name}-rerun-${generateId(3)}`, + namespace, + }, + spec: { + builderName: name, + ...rest, + }, + }) + } + + async verifyRepoReadable(url, secret, namespace) { + if (!url) return + const params = secret + ? { + remoteUrl: url, + secretRef: { + name: secret, + namespace, + }, + } + : { remoteUrl: url } + return await request.post( + `kapis/resources.kubesphere.io/v1alpha2/git/verify`, + params, + {}, + err => { + const message = get(err, 'message', '') + if (message) { + return Promise.resolve({ message }) + } + return Promise.reject(err) + } + ) + } +} diff --git a/src/stores/new_s2i/builder.js b/src/stores/new_s2i/builder.js new file mode 100644 index 00000000000..d8d65695ca6 --- /dev/null +++ b/src/stores/new_s2i/builder.js @@ -0,0 +1,256 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import { get, set, unset, uniq } from 'lodash' +import { action, observable } from 'mobx' +import Base from 'stores/base' +import { generateId } from 'utils' +import TEMPLATE from 'utils/form.templates' +import { S2I_SUPPORTED_TYPES, B2I_SUPPORTED_TYPES } from 'utils/constants' + +import S2IRunStore from './run' + +export default class S2IBuilderStore extends Base { + get apiVersion() { + return 'apis/devops.kubesphere.io/v1alpha1' + } + + module = 's2ibuilders' + + runStore = new S2IRunStore() + + @observable detail = {} + + getS2iSupportLanguage = async params => { + //调用函数获取模板 + const result = await this.getBuilderTemplate(params) + //根据模板信息计算支持的 S2I 语言类型 + const supportS2iLanguage = { + s2i: [...S2I_SUPPORTED_TYPES], + b2i: [...B2I_SUPPORTED_TYPES], + } + const b2iMap = { + java: 'jar', + tomcat: 'war', + } + if (result && result.items) { + result.items.forEach(template => { + if ( + get(template, 'metadata.labels["builder-type.kubesphere.io/b2i"]') + ) { + supportS2iLanguage.b2i.push( + b2iMap[template.spec.codeFramework] || template.spec.codeFramework + ) + } + if ( + get(template, 'metadata.labels["builder-type.kubesphere.io/s2i"]') + ) { + supportS2iLanguage.s2i.push(template.spec.codeFramework) + } + }) + } + supportS2iLanguage.s2i = uniq(supportS2iLanguage.s2i) + supportS2iLanguage.b2i = uniq(supportS2iLanguage.b2i) + return supportS2iLanguage + } + + //获取Builder模板函数 + getBuilderTemplate = async params => + await request.get( + `apis/devops.kubesphere.io/v1alpha1${this.getPath( + params + )}/s2ibuildertemplates` + ) + + // get方法获取detail + @action + fetchDetail = async ({ cluster, namespace, name }) => { + this.isLoading = true + const result = await request.get( + `apis/devops.kubesphere.io/v1alpha1${this.getPath({ + cluster, + namespace, + })}/s2ibuilders/${name}`, + undefined, + undefined, + error => { + if (error.reason === 'NotFound') { + return error + } + Promise.reject(error) + } + ) + this.detail = this.mapper(result) + this.detail.cluster = cluster + this.isLoading = false + return this.detail + } + + // 更新创建数据,对数据进行修改和处理,如生成名称、更新镜像名等操作 + updateCreateData(data, isEditor = false) { + if (!isEditor) { + const _name = `${get(data, 'spec.config.imageName', '').replace( + /[_/:]/g, + '-' + )}-${get(data, 'spec.config.tag')}` + data.metadata.name = `${_name.slice(0, 60)}-${generateId(3)}` + } + + let repoUrl = get(data, 'metadata.annotations["kubesphere.io/repoUrl"]', '') + repoUrl = repoUrl.replace(/^(http(s)?:\/\/)?(.*)$/, '$3') + + const imageName = get(data, 'spec.config.imageName', '') + + if (repoUrl && !imageName.startsWith(repoUrl)) { + const totalImageName = repoUrl.endsWith('/') + ? `${repoUrl}${imageName}` + : `${repoUrl}/${imageName}` + set(data, 'spec.config.imageName', totalImageName) + } + + if (data.isUpdateWorkload === false) { + set( + data, + 'metadata.annotations["devops.kubesphere.io/donotautoscale"]', + 'true' + ) + } else { + unset(data, 'metadata.annotations["devops.kubesphere.io/donotautoscale"]') + } + + delete data.isUpdateWorkload + } + + creatBinary(name, namespace, cluster) { + const data = TEMPLATE['b2iBuilders']({ name, namespace }) + return request.post( + `apis/devops.kubesphere.io/v1alpha1/${this.getPath({ + namespace, + cluster, + })}/s2ibinaries/${name}`, + data + ) + } + + //创建 S2I 构建,首先调用 updateCreateData 更新数据,然后调用request.post 发送 POST 请求,调用createS2IRun: 创建 S2I 运行实例。 + create(data, { cluster, namespace }) { + this.updateCreateData(data, false) + return request + .post(this.getListUrl({ cluster, namespace }), data) + .then(() => { + const binaryUrl = get(data, 'metadata.annotations.sourceUrl') + this.createS2IRun({ + cluster, + namespace, + builderName: get(data, 'metadata.name'), + binaryUrl, + }) + }) + } + + updateBuilder(data, { cluster, namespace, name }) { + this.updateCreateData(data, true) + return request + .patch(this.getDetailUrl({ cluster, namespace, name }), data) + .then(() => { + const binaryUrl = get(data, 'metadata.annotations.sourceUrl') + this.createS2IRun({ + cluster, + namespace, + builderName: get(data, 'metadata.name'), + binaryUrl, + }) + }) + } + + createS2IRun({ cluster, namespace, builderName, binaryUrl }) { + if (!builderName || !namespace) { + return + } + + const name = `${builderName.slice(0, 37)}-${generateId(3)}` + const data = { + apiVersion: 'devops.kubesphere.io/v1alpha1', + kind: 'S2iRun', + metadata: { + name, + cluster, + namespace, + }, + spec: { + builderName, + binaryUrl, + ...(binaryUrl ? { isBinaryURL: true } : {}), + }, + } + return this.runStore.create(data, { cluster, namespace }) + } + + @action + rerun = async ({ + name, + cluster, + namespace, + isUpdateWorkload = true, + ...rest + }) => { + const annotations = isUpdateWorkload + ? {} + : { + 'devops.kubesphere.io/donotautoscale': 'true', + } + return this.runStore.create({ + apiVersion: 'devops.kubesphere.io/v1alpha1', + kind: 'S2iRun', + metadata: { + annotations, + name: `${name}-rerun-${generateId(3)}`, + namespace, + }, + spec: { + builderName: name, + ...rest, + }, + }) + } + + async verifyRepoReadable(url, secret, namespace) { + if (!url) return + const params = secret + ? { + remoteUrl: url, + secretRef: { + name: secret, + namespace, + }, + } + : { remoteUrl: url } + return await request.post( + `kapis/resources.kubesphere.io/v1alpha2/git/verify`, + params, + {}, + err => { + const message = get(err, 'message', '') + if (message) { + return Promise.resolve({ message }) + } + return Promise.reject(err) + } + ) + } +} diff --git a/src/stores/new_s2i/run.js b/src/stores/new_s2i/run.js new file mode 100644 index 00000000000..7d04098f6c8 --- /dev/null +++ b/src/stores/new_s2i/run.js @@ -0,0 +1,265 @@ +/* + * This file is part of KubeSphere Console. + * Copyright (C) 2019 The KubeSphere Console Authors. + * + * KubeSphere Console is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KubeSphere Console is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with KubeSphere Console. If not, see . + */ + +import { action, observable } from 'mobx' +import { Notify } from '@kube-design/components' +import ObjectMapper from 'utils/object.mapper' +import { isArray, get } from 'lodash' +import { getFilterString, parseUrl } from 'utils' + +import Base from '../base' + +export default class S2irunStore extends Base { + @observable + list = { + data: [], + page: 1, + limit: 10, + total: 0, + isLoading: true, + selectedRowKeys: [], + } + + @observable + logData = { + isLoading: true, + log: '', + start: 0, + hasMore: true, + } + + @observable + jobDetail = {} + + @observable + runDetail = {} + + @observable + isLoading = true + + constructor() { + super() + this.module = 's2iruns' + } + + get apiVersion() { + return 'apis/devops.kubesphere.io/v1alpha1' + } + + @action + async fetchJobDetail({ name, cluster, namespace }) { + this.isLoading = true + + const result = await request.get( + `apis/batch/v1${this.getPath({ cluster, namespace })}/jobs/${name}` + ) + const detail = ObjectMapper['jobs'](result) + this.jobDetail = detail + this.isLoading = false + } + + @action + async fetchRunDetail({ cluster, namespace, runName }) { + if (!runName) { + return + } + + this.getRunDetailLoading = true + const result = await request.get( + `apis/devops.kubesphere.io/v1alpha1${this.getPath({ + cluster, + namespace, + })}/${this.module}/${runName}` + ) + + this.runDetail = ObjectMapper['s2iruns'](result) + this.getRunDetailLoading = false + return this.runDetail + } + + @action + async deleteRun({ cluster, namespace, runName }) { + if (!runName) { + return + } + + await request.delete( + `apis/devops.kubesphere.io/v1alpha1${this.getPath({ + cluster, + namespace, + })}/${this.module}/${runName}` + ) + } + + //获取s2i运行记录的信息 + @action + async fetchS2IRunRecords({ + limit = 10, + name, + page = 1, + order, + reverse, + workspace, + cluster, + namespace, + more, + ...filters + } = {}) { + if (!this.list.data.length) { + this.list.isLoading = true + } + const params = {} + + params.conditions = getFilterString({ + ...filters, + 'labels.devops.kubesphere.io/builder-name': name, + }) + + if (!order && reverse === undefined) { + order = 'createTime' + reverse = true + } + + if (limit !== Infinity) { + params.paging = `limit=${limit},page=${page}` + } + + if (order) { + params.orderBy = order + } + + params.reverse = true + + const result = await request.get( + `kapis/resources.kubesphere.io/v1alpha2${this.getPath({ + cluster, + namespace, + })}/${this.module}`, + params + ) + const data = result.items.map(this.mapper) + data.forEach((item, index) => { + item.count = result.total_count - index - 10 * (page - 1) + item.cluster = cluster + }) + + this.list = { + data: more ? [...this.list.data, ...data] : data, + total: result.total_count || 0, + limit: Number(limit) || 10, + page: Number(page) || 1, + order, + reverse, + filters, + isLoading: false, + selectedRowKeys: [], + } + return this.list + } + + @action + getLog = async (logURL, cluster) => { + if (this.logData.logURL !== logURL) { + this.logData = { + isLoading: true, + log: '', + start: 0, + hasMore: true, + } + } + let url = parseUrl(logURL).pathname.slice(1) + + const namespaces = get(url.match(/\/namespaces\/(.*)\/pods\//), '1') + const pods = get(url.match(/\/pods\/(.*)/), '1') + + url = `kapis/tenant.kubesphere.io/v1alpha2${this.getPath({ + cluster, + })}/logs` + + const result = await request.get(url, { + namespaces, + pods, + container: this.containerName, + timestamps: true, + tailLines: 1000, + size: 300, + from: this.logData.start, + sort: 'asc', + }) + + const logRecords = get(result, 'query.records', []) + const total = get(result, 'query.total', []) + const { log } = this.logData + + if (isArray(logRecords)) { + const totalLog = logRecords.reduce( + (logStr, logItem) => logStr + logItem.log, + log + ) + this.logData = { + logURL, + log: totalLog, + start: this.logData.start + logRecords.length, + hasMore: total > this.logData.start + logRecords.length, + isLoading: false, + } + } + } + + @action + async fetchPodsLogs(logURL, cluster) { + if (get(this.logData, 'logURL', '') !== logURL) { + this.logData = { + isLoading: false, + log: '', + start: 0, + hasMore: false, + } + } + this.logData.isLoading = true + const namespace = get(logURL.match(/namespaces\/([\w-/.]*)*\?/), '1') + if (!this.containerName) { + const podsDetail = await request.get( + `api/v1${this.getPath({ namespace, cluster })}` + ) + const containerID = get( + podsDetail, + 'status.containerStatuses[0]containerID' + ) + if (!containerID) { + return Notify.error('container not ready') + } + this.containerName = get(podsDetail, 'spec.containers[0].name', '') + } + const result = await request.get( + `api/v1${this.getPath({ namespace, cluster })}/log`, + { + container: this.containerName, + timestamps: true, + tailLines: 1000, + } + ) + this.logData = { + logURL, + isLoading: false, + log: result, + start: 0, + hasMore: false, + } + } +} diff --git a/src/stores/s2i/builder.js b/src/stores/s2i/builder.js index f5c62b7b917..d8d65695ca6 100644 --- a/src/stores/s2i/builder.js +++ b/src/stores/s2i/builder.js @@ -37,7 +37,9 @@ export default class S2IBuilderStore extends Base { @observable detail = {} getS2iSupportLanguage = async params => { + //调用函数获取模板 const result = await this.getBuilderTemplate(params) + //根据模板信息计算支持的 S2I 语言类型 const supportS2iLanguage = { s2i: [...S2I_SUPPORTED_TYPES], b2i: [...B2I_SUPPORTED_TYPES], @@ -66,7 +68,8 @@ export default class S2IBuilderStore extends Base { supportS2iLanguage.b2i = uniq(supportS2iLanguage.b2i) return supportS2iLanguage } - + + //获取Builder模板函数 getBuilderTemplate = async params => await request.get( `apis/devops.kubesphere.io/v1alpha1${this.getPath( @@ -74,6 +77,7 @@ export default class S2IBuilderStore extends Base { )}/s2ibuildertemplates` ) + // get方法获取detail @action fetchDetail = async ({ cluster, namespace, name }) => { this.isLoading = true @@ -97,6 +101,7 @@ export default class S2IBuilderStore extends Base { return this.detail } + // 更新创建数据,对数据进行修改和处理,如生成名称、更新镜像名等操作 updateCreateData(data, isEditor = false) { if (!isEditor) { const _name = `${get(data, 'spec.config.imageName', '').replace( @@ -142,6 +147,7 @@ export default class S2IBuilderStore extends Base { ) } + //创建 S2I 构建,首先调用 updateCreateData 更新数据,然后调用request.post 发送 POST 请求,调用createS2IRun: 创建 S2I 运行实例。 create(data, { cluster, namespace }) { this.updateCreateData(data, false) return request diff --git a/src/stores/s2i/run.js b/src/stores/s2i/run.js index cd29b20ce27..7d04098f6c8 100644 --- a/src/stores/s2i/run.js +++ b/src/stores/s2i/run.js @@ -106,6 +106,7 @@ export default class S2irunStore extends Base { ) } + //获取s2i运行记录的信息 @action async fetchS2IRunRecords({ limit = 10,