diff --git a/locales/en/l10n-projects-imageBuilders-list.js b/locales/en/l10n-projects-imageBuilders-list.js
index 19ea12241e5..7c7dc1ce9b0 100644
--- a/locales/en/l10n-projects-imageBuilders-list.js
+++ b/locales/en/l10n-projects-imageBuilders-list.js
@@ -70,6 +70,8 @@ module.exports = {
IMAGE_NAME_EMPTY_DESC: 'Please enter an image name.',
IMAGE_TAG_EMPTY_DESC: 'Please enter an image tag.',
TARGET_IMAGE_REPOSITORY_EMPTY_DESC: 'Please set a target image registry.',
- // List > Edit Information
- // List > Delete
+ VALIDATE_SUCCESS: 'Validation succeeded',
+ VALIDATE_FAILED: 'Validation failed',
+ RUN_SUCCESSFUL: 'Run succeeded',
+ RUN_FAILED: 'Run failed',
}
diff --git a/locales/zh/l10n-projects-imageBuilders-list.js b/locales/zh/l10n-projects-imageBuilders-list.js
index 1fc77036d2c..817849f6e2b 100644
--- a/locales/zh/l10n-projects-imageBuilders-list.js
+++ b/locales/zh/l10n-projects-imageBuilders-list.js
@@ -68,5 +68,9 @@ module.exports = {
WRONG_FILE_EXTENSION_NAME: '选择的文件类型不匹配,请选择 {type} 类型。',
IMAGE_NAME_EMPTY_DESC: '请输入镜像名称。',
IMAGE_TAG_EMPTY_DESC: '请输入镜像标签。',
- TARGET_IMAGE_REPOSITORY_EMPTY_DESC: '请设置目标镜像服务。'
+ TARGET_IMAGE_REPOSITORY_EMPTY_DESC: '请设置目标镜像服务。',
+ VALIDATE_SUCCESS: '校验成功',
+ VALIDATE_FAILED: '校验失败',
+ RUN_SUCCESSFUL: '运行成功',
+ RUN_FAILED: '运行失败',
};
\ No newline at end of file
diff --git a/server/config.yaml b/server/config.yaml
index 6a20f0bade7..0473add82f2 100644
--- a/server/config.yaml
+++ b/server/config.yaml
@@ -522,6 +522,12 @@ client:
authKey: "gitrepositories",
requiredClusterVersion: v3.3.0,
}
+ - {
+ name: imageBuilders,
+ title: IMAGE_BUILDER_PL,
+ icon: vnas,
+ clusterModule: imagebuilds,
+ }
- name: management
title: DEVOPS_PROJECT_SETTINGS
icon: cogwheel
diff --git a/src/actions/devopsImageBuilder.js b/src/actions/devopsImageBuilder.js
new file mode 100644
index 00000000000..4ab8b27a863
--- /dev/null
+++ b/src/actions/devopsImageBuilder.js
@@ -0,0 +1,97 @@
+/*
+ * 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 { Notify } from '@kube-design/components'
+import { Modal } from 'components/Base'
+
+import CreateModal from 'components/Modals/Create'
+import EditBasicInfoModal from 'components/Modals/EditBasicInfo'
+import FORM_STEPS from 'configs/steps/devopsImageBuilder'
+import { get, set } from 'lodash'
+import moment from 'moment-mini'
+
+export default {
+ 'devops.imagebuilder.create': {
+ on({ store, cluster, namespace, module, success, ...props }) {
+ const formTemplate = store.getFormTemplate({
+ namespace,
+ })
+
+ const modal = Modal.open({
+ onOk: data => {
+ if (!data) {
+ return
+ }
+
+ set(data, 'spec.source.revisionId', undefined)
+ set(
+ data,
+ 'spec.output.image',
+ `${get(data, 'spec.output.image')}:${get(data, 'spec.output.tag')}`
+ )
+ const now = moment().format('YYYYMMDDHHmmss')
+ set(
+ data,
+ 'metadata.name',
+ `${get(
+ data,
+ 'metadata.annotations.languageType',
+ 'image-builder'
+ )}-${now}-${Math.random()
+ .toString(32)
+ .slice(2)}`
+ )
+ store.create(data, { cluster, namespace }).then(() => {
+ Modal.close(modal)
+ Notify.success({ content: t('CREATE_SUCCESSFUL') })
+ success && success()
+ })
+ },
+ module,
+ cluster,
+ namespace,
+ hideB2i: true,
+ hideAdvanced: true,
+ name: 'IMAGE_BUILDER',
+ formTemplate,
+ steps: FORM_STEPS,
+ modal: CreateModal,
+ noCodeEdit: true,
+ store,
+ ...props,
+ })
+ },
+ },
+ 'devops.imagebuilder.baseinfo.edit': {
+ on({ store, detail, success, ...props }) {
+ const modal = Modal.open({
+ onOk: _data => {
+ store.update(detail, _data).then(() => {
+ Modal.close(modal)
+ Notify.success({ content: t('UPDATE_SUCCESSFUL') })
+ success && success()
+ })
+ },
+ detail,
+ modal: EditBasicInfoModal,
+ store,
+ ...props,
+ })
+ },
+ },
+}
diff --git a/src/components/Forms/DevopsImageBuilder/LanguageSelect/index.jsx b/src/components/Forms/DevopsImageBuilder/LanguageSelect/index.jsx
new file mode 100644
index 00000000000..3605ddac3e4
--- /dev/null
+++ b/src/components/Forms/DevopsImageBuilder/LanguageSelect/index.jsx
@@ -0,0 +1,137 @@
+/*
+ * 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 { set, get, unset } from 'lodash'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { Form, Icon, Alert, Input } from '@kube-design/components'
+import S2iBuilderStore from 'stores/devops/imgBuilder'
+import { getLanguageIcon } from 'utils/devops'
+
+import S2IForm from '../S2IForm'
+import styles from './index.scss'
+
+export default class LanguageSelect extends React.Component {
+ constructor(props) {
+ super(props)
+ this.store = new S2iBuilderStore()
+ this.state = {
+ s2i: [],
+ b2i: [],
+ }
+ }
+
+ static contextTypes = {
+ setSteps: PropTypes.func,
+ }
+
+ componentDidMount() {
+ this.fetchData()
+ }
+
+ fetchData = async () => {
+ const { cluster } = this.props
+ const supportS2iLanguage = await this.store.getS2iSupportLanguage({
+ cluster,
+ })
+ this.setState(supportS2iLanguage)
+ }
+
+ handleLanguageSelect = languageType => () => {
+ const { steps } = this.props
+
+ set(steps, '[1].component', S2IForm)
+ set(
+ this.props.formTemplate,
+ 'metadata.labels["s2i-type.kubesphere.io"]',
+ 's2i'
+ )
+ unset(this.props.formTemplate, 'spec.config.isBinaryURL')
+ this.context.setSteps(steps)
+
+ set(
+ this.props.formTemplate,
+ 'metadata.annotations.languageType',
+ languageType
+ )
+
+ unset(this.props.formTemplate, 'spec.config.builderImage')
+
+ this.forceUpdate()
+ }
+
+ renderSupportTip = () => {
+ if (globals.runtime.toLowerCase() === 'containerd') {
+ return (
+
+ )
+ }
+ }
+
+ render() {
+ const { formTemplate, formRef } = this.props
+ const languageType = get(
+ this.props.formTemplate,
+ 'metadata.annotations.languageType'
+ )
+ const buildType = get(
+ this.props.formTemplate,
+ 'metadata.labels["s2i-type.kubesphere.io"]',
+ 's2i'
+ )
+ return (
+
+
+
+
+ )
+ }
+}
diff --git a/src/components/Forms/DevopsImageBuilder/LanguageSelect/index.scss b/src/components/Forms/DevopsImageBuilder/LanguageSelect/index.scss
new file mode 100644
index 00000000000..ed2c3b2139c
--- /dev/null
+++ b/src/components/Forms/DevopsImageBuilder/LanguageSelect/index.scss
@@ -0,0 +1,52 @@
+@import '~scss/variables';
+@import '~scss/mixins';
+
+.header {
+ margin-bottom: 12px;
+ p:first-child {
+ height: 20px;
+ @include TypographyTitleH6;
+ }
+ p:last-child {
+ height: 20px;
+ @include TypographyParagraph($dark-color01);
+ }
+}
+
+.content {
+ display: flex;
+ margin-bottom: 20px;
+
+ li {
+ margin-right: 12px;
+ padding: 14px;
+ width: 134px;
+ height: 100px;
+ border-radius: 4px;
+ border: solid 1px $border-color;
+ background-color: #ffffff;
+ text-align: center;
+
+ &:hover {
+ box-shadow: 0 4px 8px 0 rgba(36, 46, 66, 0.2);
+ border: solid 1px $dark-color01;
+ }
+
+ img {
+ width: 46px;
+ }
+ p {
+ height: 20px;
+ @include TypographyTitleH6;
+ }
+ }
+}
+
+.item_select {
+ box-shadow: 0 4px 8px 0 rgba(36, 46, 66, 0.2);
+ border: solid 1px $dark-color01 !important;
+}
+
+.margin_b_10 {
+ margin-bottom: 10px;
+}
diff --git a/src/components/Forms/DevopsImageBuilder/RerunForm/index.jsx b/src/components/Forms/DevopsImageBuilder/RerunForm/index.jsx
new file mode 100644
index 00000000000..b243f5d0f8a
--- /dev/null
+++ b/src/components/Forms/DevopsImageBuilder/RerunForm/index.jsx
@@ -0,0 +1,133 @@
+/*
+ * 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 PropTypes from 'prop-types'
+import { get, set } from 'lodash'
+import { Checkbox, Form } from '@kube-design/components'
+import { Modal } from 'components/Base'
+
+import S2iForm from '../S2IForm'
+
+import styles from './index.scss'
+
+export default class RerunForm extends React.Component {
+ static propTypes = {
+ visible: PropTypes.bool,
+ onOk: PropTypes.func,
+ onCancel: PropTypes.func,
+ isSubmitting: PropTypes.bool,
+ }
+
+ static defaultProps = {
+ visible: false,
+ onOk() {},
+ onCancel() {},
+ isSubmitting: false,
+ }
+
+ constructor(props) {
+ super(props)
+
+ this.form = React.createRef()
+ this.content = React.createRef()
+ }
+
+ handleOk = () => {
+ const { onOk, detail } = this.props
+
+ if (detail) {
+ if (
+ get(
+ detail,
+ "metadata.annotations['devops.kubesphere.io/donotautoscale']"
+ )
+ ) {
+ set(
+ detail,
+ "metadata.annotations['devops.kubesphere.io/donotautoscale']",
+ 'true'
+ )
+ }
+ this.content.current.validate(() => {
+ onOk(detail)
+ })
+ }
+ }
+
+ renderEnableUpdate = () => {
+ const { detail } = this.props
+
+ if (!get(detail, 'metadata.annotations.serviceName')) {
+ return null
+ }
+
+ return (
+
+
+
{t('S2I_UPDATA_WORKLOAD_DESC')}
+
+ )
+ }
+
+ render() {
+ const {
+ visible,
+ isSubmitting,
+ onCancel,
+ cluster,
+ detail,
+ namespace,
+ } = this.props
+ const isB2i = get(detail, 'spec.config.isBinaryURL')
+
+ return (
+
+ {isB2i ? null : (
+
+ )}
+ {this.renderEnableUpdate()}
+
+ )
+ }
+}
diff --git a/src/components/Forms/DevopsImageBuilder/RerunForm/index.scss b/src/components/Forms/DevopsImageBuilder/RerunForm/index.scss
new file mode 100644
index 00000000000..49076dd03fe
--- /dev/null
+++ b/src/components/Forms/DevopsImageBuilder/RerunForm/index.scss
@@ -0,0 +1,23 @@
+@import '~scss/variables';
+
+.checkboxCard {
+ margin-top: 20px;
+ height: 64px;
+ padding: 10px 15px;
+ border-radius: 4px;
+ border: solid 1px $border-color;
+ background-color: #ffffff;
+ label {
+ .title {
+ font-weight: 600;
+ color: #242e42;
+ }
+ height: 20px;
+ }
+ .desc {
+ color: #79879c;
+ }
+ &:hover {
+ box-shadow: 0 8px 16px 0 rgba(36, 46, 66, 0.08);
+ }
+}
diff --git a/src/components/Forms/DevopsImageBuilder/S2IForm/TemplateSelect/index.jsx b/src/components/Forms/DevopsImageBuilder/S2IForm/TemplateSelect/index.jsx
new file mode 100644
index 00000000000..e3a88d40dbf
--- /dev/null
+++ b/src/components/Forms/DevopsImageBuilder/S2IForm/TemplateSelect/index.jsx
@@ -0,0 +1,91 @@
+/*
+ * 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 { Form, Loading } from '@kube-design/components'
+import { TypeSelect } from 'components/Base'
+import { get } from 'lodash'
+import React from 'react'
+
+import styles from './index.scss'
+
+export default class TemplateSelect extends React.PureComponent {
+ static defaultProps = {
+ builderTemplate: [],
+ onEnvironmentChange: () => {},
+ }
+
+ get languageType() {
+ return get(this.props.formTemplate, 'metadata.annotations.languageType')
+ }
+
+ get containerList() {
+ const { builderTemplate } = this.props
+ return builderTemplate.reduce((options, currentTemplate) => {
+ return options.concat(this.getOptions(currentTemplate))
+ }, [])
+ }
+
+ getOptions(template) {
+ return {
+ ...template,
+ type: get(template, 'metadata.labels.language', ''),
+ description: get(template, 'spec.description', '-'),
+ environment: '',
+ builderImage: get(template, 'metadata.name'),
+ }
+ }
+
+ getDefaultValue = () => {
+ return this.containerList?.[0]?.builderImage
+ }
+
+ getTemplateOptions() {
+ return this.containerList.map(container => ({
+ icon: container.type,
+ value: container.builderImage,
+ label: container.builderImage,
+ description: container.description,
+ }))
+ }
+
+ render() {
+ const { loading } = this.props
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/src/components/Forms/DevopsImageBuilder/S2IForm/TemplateSelect/index.scss b/src/components/Forms/DevopsImageBuilder/S2IForm/TemplateSelect/index.scss
new file mode 100644
index 00000000000..2e5bd93abe4
--- /dev/null
+++ b/src/components/Forms/DevopsImageBuilder/S2IForm/TemplateSelect/index.scss
@@ -0,0 +1,16 @@
+@import '~scss/variables';
+@import '~scss/mixins';
+
+.form {
+ margin-bottom: 12px;
+}
+
+.leftIcon {
+ width: 30px;
+ position: absolute;
+ @include vertical-center;
+ left: 16px;
+}
+.loading {
+ width: 100%;
+}
diff --git a/src/components/Forms/DevopsImageBuilder/S2IForm/index.jsx b/src/components/Forms/DevopsImageBuilder/S2IForm/index.jsx
new file mode 100644
index 00000000000..b4dc72d9f00
--- /dev/null
+++ b/src/components/Forms/DevopsImageBuilder/S2IForm/index.jsx
@@ -0,0 +1,315 @@
+/*
+ * 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 { Form, Input, Loading, Select } from '@kube-design/components'
+import classnames from 'classnames'
+import ToggleView from 'components/ToggleView'
+import { get } from 'lodash'
+import React from 'react'
+import SecretStore from 'stores/secret'
+import BuilderStore from 'stores/devops/imgBuilder'
+import { getDisplayName, getDocsUrl } from 'utils'
+import { PATTERN_IMAGE_NAME } from 'utils/constants'
+
+import TemplateSelect from './TemplateSelect'
+
+import styles from './index.scss'
+
+export default class S2IForm extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.secretStore = new SecretStore()
+ this.builderStore = new BuilderStore()
+
+ this.state = {
+ isGetTemplateListLoading: true,
+ environment: [],
+ basicSecretOptions: [],
+ imageSecretOptions: [],
+ repoReadError: null,
+ repoNeedSecret: true,
+ readRepoLoading: false,
+ docUrl: '',
+ }
+ }
+
+ static defaultProps = {
+ mode: 'create',
+ prefix: '',
+ }
+
+ get prefix() {
+ const { prefix } = this.props
+ return prefix ? `${prefix}.` : ''
+ }
+
+ componentDidMount() {
+ this.fetchData()
+ this.fetchImageSecrets()
+ this.getTemplateList()
+ this.handleRepoReadableCheck()
+ }
+
+ get namespace() {
+ return this.props.namespace
+ }
+
+ fetchImageSecrets = async () => {
+ const results = await this.secretStore.fetchListByK8s({
+ namespace: this.namespace,
+ cluster: this.props.cluster,
+ fieldSelector: `type=kubernetes.io/dockerconfigjson`,
+ })
+
+ const imageSecretOptions = results.map(item => {
+ const auths = get(item, 'data[".dockerconfigjson"].auths', {})
+ const repoUrl = Object.keys(auths)[0] || ''
+ return {
+ label: getDisplayName(item),
+ value: item.name,
+ repoUrl,
+ type: 'dockerconfigjson',
+ }
+ })
+
+ this.setState({ imageSecretOptions })
+ }
+
+ fetchData = async () => {
+ const results = await this.secretStore.fetchListByK8s({
+ namespace: this.namespace,
+ cluster: this.props.cluster,
+ fieldSelector: `type=kubernetes.io/basic-auth`,
+ })
+
+ const basicSecretOptions = results.map(item => ({
+ label: getDisplayName(item),
+ value: item.name,
+ type: 'basic-auth',
+ }))
+
+ this.setState({ basicSecretOptions })
+ }
+
+ getTemplateList = async () => {
+ this.setState({ isGetTemplateListLoading: true })
+ const lists = await this.builderStore.getBuilderTemplate({
+ cluster: this.props.cluster,
+ limit: -1,
+ language: get(
+ this.props.formTemplate,
+ 'metadata.annotations.languageType'
+ ),
+ })
+ this.setState({
+ builderTemplateLists: lists ?? [],
+ isGetTemplateListLoading: false,
+ })
+ }
+
+ handleImageTemplateChange = ({ environment, docUrl }) => {
+ const lang = get(globals, 'user.lang', 'zh')
+
+ const EnvOptions = (environment || []).map(env => {
+ const descArr = (env.description || '').split('. ')
+ const desc =
+ lang === 'zh'
+ ? get(descArr, '1', env.description)
+ : get(descArr, '0', env.description)
+ env.label = `${env.key} (${desc})`
+ env.value = env.key
+ return env
+ })
+ this.setState({ environment: EnvOptions, docUrl })
+ }
+
+ handleRepoReadableCheck = async () => {
+ const { formTemplate } = this.props
+ const sourceUrl = get(formTemplate, `${this.prefix}spec.source.url`, '')
+ if (!sourceUrl) return
+ this.setState({ readRepoLoading: true })
+ const secret = get(
+ formTemplate,
+ `${this.prefix}spec.source.credentials.name`,
+ ''
+ )
+ const resp = await this.builderStore
+ .verifyRepoReadable(sourceUrl, secret, this.namespace)
+ .finally(() => {
+ this.setState({ readRepoLoading: false })
+ })
+ const message = get(resp, 'message', '')
+
+ if (message === 'success') {
+ this.setState({
+ repoReadError: null,
+ })
+ if (!secret) {
+ this.setState({
+ repoNeedSecret: false,
+ })
+ }
+ return
+ }
+ this.setState({
+ repoReadError: { message: t(message) },
+ repoNeedSecret: true,
+ })
+ }
+
+ handleSecretChange = () => {
+ this.handleRepoReadableCheck()
+ }
+
+ renderAdvancedSetting() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+ }
+
+ render() {
+ const { formTemplate, formRef, mode, prefix } = this.props
+
+ return (
+
+ )
+ }
+}
diff --git a/src/components/Forms/DevopsImageBuilder/S2IForm/index.scss b/src/components/Forms/DevopsImageBuilder/S2IForm/index.scss
new file mode 100644
index 00000000000..c096afc4fee
--- /dev/null
+++ b/src/components/Forms/DevopsImageBuilder/S2IForm/index.scss
@@ -0,0 +1,53 @@
+@import '~scss/variables';
+@import '~scss/mixins';
+
+@mixin column {
+ flex: none;
+ flex-grow: 1;
+ flex-shrink: 1;
+ padding: 12px;
+}
+
+.columns {
+ display: flex;
+ margin-left: -12px;
+ margin-right: -12px;
+ margin-top: -12px;
+
+ .column {
+ @include column;
+ }
+
+ :global(.is-2) {
+ width: 20%;
+ min-width: 20%;
+ max-width: 20%;
+ @include column;
+ }
+ :global(.is-half) {
+ width: 50%;
+ min-width: 50%;
+ max-width: 50%;
+ @include column;
+ }
+}
+
+.columsEdit {
+ flex-wrap: wrap;
+ :global(.is-half) {
+ width: 100%;
+ min-width: 100%;
+ max-width: 100%;
+ @include column;
+ }
+}
+
+.margin_b_10 {
+ margin-bottom: 10px;
+}
+
+.disabled {
+ opacity: 0.3;
+ transition: opacity 1.5 linear;
+ pointer-events: none;
+}
diff --git a/src/configs/steps/devopsImageBuilder.js b/src/configs/steps/devopsImageBuilder.js
new file mode 100644
index 00000000000..3b22521791f
--- /dev/null
+++ b/src/configs/steps/devopsImageBuilder.js
@@ -0,0 +1,32 @@
+/*
+ * 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 LanguageSelect from 'components/Forms/DevopsImageBuilder/LanguageSelect'
+import S2IForm from 'components/Forms/DevopsImageBuilder/S2IForm'
+
+export default [
+ {
+ title: 'BUILD_MODE',
+ component: LanguageSelect,
+ },
+ {
+ title: 'BUILD_SETTINGS',
+ component: S2IForm,
+ required: true,
+ },
+]
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..3d2fb4c9a71
--- /dev/null
+++ b/src/pages/devops/containers/ImageBuilder/Detail/BuildRecords/index.jsx
@@ -0,0 +1,70 @@
+/*
+ * 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 } from 'lodash'
+import { inject, observer } from 'mobx-react'
+import React from 'react'
+
+import ImageBuilderLastRun from 'projects/components/Cards/ImageBuilderLastRun'
+import DevopsRunsCard from 'projects/components/Cards/ImageRunRecord/devops'
+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 detail = this.store.detail
+ 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..2fae7f93153
--- /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;
+}
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..d29464ef531
--- /dev/null
+++ b/src/pages/devops/containers/ImageBuilder/Detail/index.jsx
@@ -0,0 +1,202 @@
+/*
+ * 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, isArray } from 'lodash'
+import { toJS } from 'mobx'
+import { inject, observer } from 'mobx-react'
+import React from 'react'
+
+import { Loading, Notify } from '@kube-design/components'
+
+import S2IRunStore from 'stores/devops/imageBuilderRun'
+import S2IBuilderStore from 'stores/devops/imgBuilder'
+import ResourceStore from 'stores/workload/resource'
+import { getDisplayName, getLocalTime, showNameAndAlias } from 'utils'
+import { trigger } from 'utils/action'
+
+import DetailPage from 'projects/containers/Base/Detail'
+
+import getRoutes from './routes'
+
+@inject('rootStore')
+@observer
+@trigger
+export default class ImageBuilderDetail extends React.Component {
+ store = new S2IBuilderStore()
+
+ resourceStore = new ResourceStore(this.module)
+
+ s2iRunStore = new S2IRunStore(this.name)
+
+ componentDidMount() {
+ this.fetchData()
+ }
+
+ get module() {
+ return this.store.module
+ }
+
+ get name() {
+ return this.props.match.params.name
+ }
+
+ get routing() {
+ return this.props.rootStore.routing
+ }
+
+ get params() {
+ return {
+ ...this.props.match.params,
+ namespace: this.props.match.params.devops,
+ }
+ }
+
+ get listUrl() {
+ const { workspace, cluster, devops: namespace } = this.props.match.params
+ return `${
+ workspace ? `/${workspace}` : ''
+ }/clusters/${cluster}/devops/${namespace}/imageBuilders`
+ }
+
+ fetchData = async () => {
+ this.store.fetchDetail({ ...this.params })
+ this.s2iRunStore.fetchListByK8s({
+ cluster: this.props.match.params.cluster,
+ namespace: this.props.match.params.devops,
+ })
+ }
+
+ handleCopy = () => {
+ Notify.success({
+ content: t('COPIED_SUCCESSFUL'),
+ })
+ }
+
+ getOperations = () => [
+ {
+ key: 'Run',
+ text: t('RUN'),
+ action: 'edit',
+ type: 'control',
+ onClick: () =>
+ this.store.run(toJS(this.store.detail)).then(this.fetchData),
+ },
+ {
+ key: 'edit',
+ icon: 'pen',
+ text: t('EDIT_INFORMATION'),
+ action: 'edit',
+ onClick: () =>
+ this.trigger('devops.imagebuilder.baseinfo.edit', {
+ type: this.name,
+ detail: toJS(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 { spec = {} } = detail
+
+ return [
+ {
+ name: t('NAME'),
+ value: detail.name,
+ },
+ {
+ name: t('PROJECT'),
+ value: showNameAndAlias(detail.namespace, 'project'),
+ },
+ {
+ name: t('TYPE'),
+ value: t(detail.type),
+ },
+ {
+ name: t('BUILDER_IMAGE'),
+ value: get(spec, 'output.image', '-'),
+ },
+ {
+ name: t('CODE_REPOSITORY_URL'),
+ value: get(spec, 'source.url', '-'),
+ },
+ {
+ 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.list.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..df74e7a9555
--- /dev/null
+++ b/src/pages/devops/containers/ImageBuilder/Detail/index.scss
@@ -0,0 +1,10 @@
+@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..2c935208ffe
--- /dev/null
+++ b/src/pages/devops/containers/ImageBuilder/Detail/routes.js
@@ -0,0 +1,31 @@
+/*
+ * 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'
+
+export default path => [
+ {
+ path: `${path}/records`,
+ title: 'RUN_RECORDS',
+ component: ImageBuildRecords,
+ 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..cabe5b466e1
--- /dev/null
+++ b/src/pages/devops/containers/ImageBuilder/index.jsx
@@ -0,0 +1,208 @@
+/*
+ * 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 { 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 { get } from 'lodash'
+// import { reaction } from 'mobx'
+import { inject } from 'mobx-react'
+import React from 'react'
+
+import { getDisplayName, getLocalTime } from 'utils'
+import { ICON_TYPES } from 'utils/constants'
+
+import S2IBuilderStore from 'stores/devops/imgBuilder'
+import { Notify } from '@kube-design/components'
+
+@inject('rootStore')
+@inject('devopsStore')
+@withList({
+ store: new S2IBuilderStore(),
+ module: 'ImgBuilders',
+ name: 'IMAGE_BUILDER',
+})
+export default class ImageBuilders extends React.Component {
+ get store() {
+ return this.props.store
+ }
+
+ get devops() {
+ return this.props.match.params.devops
+ }
+
+ // 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('devops.imagebuilder.baseinfo.edit', {
+ detail: item,
+ }),
+ },
+ {
+ key: 'Run',
+ text: t('RUN'),
+ icon: 'triangle-right',
+ action: 'edit',
+ type: 'edit',
+ onClick: detail => {
+ this.store.run(detail).then(() => {
+ Notify.success({ content: t('RUN_SUCCESS') })
+ this.routing.query()
+ })
+ },
+ },
+ {
+ key: 'delete',
+ icon: 'trash',
+ text: t('DELETE'),
+ action: 'delete',
+ onClick: item =>
+ trigger('resource.delete', {
+ type: name,
+ detail: item,
+ success: this.routing.query,
+ }),
+ },
+ ]
+ }
+
+ 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 => {
+ 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'),
+ },
+ ]
+ }
+
+ get routing() {
+ return this.props.rootStore.routing
+ }
+
+ showCreate = () => {
+ const { match, module, devopsStore } = this.props
+ return this.props.trigger('devops.imagebuilder.create', {
+ module,
+ projectDetail: devopsStore.detail,
+ namespace: this.devops,
+ cluster: match.params.cluster,
+ success: this.routing.query,
+ })
+ }
+
+ getData = params => {
+ const { getData: fn } = this.props
+ return fn({ ...params, devops: undefined, namespace: this.devops })
+ }
+
+ render() {
+ const { bannerProps, tableProps } = this.props
+ return (
+
+
+
+ )
+ }
+}
diff --git a/src/stores/devops/imageBuildStrategies.js b/src/stores/devops/imageBuildStrategies.js
new file mode 100644
index 00000000000..e4eef8c060e
--- /dev/null
+++ b/src/stores/devops/imageBuildStrategies.js
@@ -0,0 +1,27 @@
+/*
+ * 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 BaseStore from 'stores/base'
+
+export default class ImageBuildStrategiesStore extends BaseStore {
+ module = 'imagebuildStrategies'
+
+ get apiVersion() {
+ return 'kapis/builder.kubesphere.io/v1alpha1'
+ }
+}
diff --git a/src/stores/devops/imageBuilderRun.js b/src/stores/devops/imageBuilderRun.js
new file mode 100644
index 00000000000..58a90bb56a0
--- /dev/null
+++ b/src/stores/devops/imageBuilderRun.js
@@ -0,0 +1,59 @@
+/*
+ * 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 } from 'lodash'
+import { observable } from 'mobx'
+import BaseStore from 'stores/base'
+import objectMapper from 'utils/object.mapper'
+
+export default class ImageBuildRunStore extends BaseStore {
+ module = 'imagebuildRuns'
+
+ constructor(name) {
+ super()
+ this.imageBuilderName = name
+ }
+
+ @observable
+ imageBuilderName = ''
+
+ get apiVersion() {
+ return 'kapis/builder.kubesphere.io/v1alpha1'
+ }
+
+ getListUrl = (params = {}) => {
+ const path = `${this.apiVersion}${this.getPath(params)}/imagebuilds/${
+ this.imageBuilderName
+ }/${this.module}`
+ return path
+ }
+
+ get mapper() {
+ return item => {
+ return {
+ ...objectMapper.default(item),
+ imageName: get(item, 'status.buildSpec.source.url'),
+ status: get(item, 'status.conditions', []).every(
+ i => i.status !== 'False'
+ )
+ ? 'Successful'
+ : 'Failed',
+ }
+ }
+ }
+}
diff --git a/src/stores/devops/imgBuilder.js b/src/stores/devops/imgBuilder.js
new file mode 100644
index 00000000000..deaea85dd47
--- /dev/null
+++ b/src/stores/devops/imgBuilder.js
@@ -0,0 +1,117 @@
+/*
+ * 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 } from 'lodash'
+import { action } from 'mobx'
+import BaseStore from 'stores/base'
+import ObjectMapper from 'utils/object.mapper'
+import request from 'utils/request'
+import ImageBuildStrategiesStore from './imageBuildStrategies'
+
+export default class ImgBuilderStore extends BaseStore {
+ constructor() {
+ super()
+ this.module = 'imagebuilds'
+ this.stategies = new ImageBuildStrategiesStore()
+ }
+
+ get apiVersion() {
+ return 'kapis/builder.kubesphere.io/v1alpha1'
+ }
+
+ getResourceUrl = (params = {}) =>
+ `${this.apiVersion}${this.getPath(params)}/${this.module}`
+
+ getFormTemplate = ({ namespace, languageType = '' }) => ({
+ apiVersion: 'shipwright.io/v1alpha1',
+ kind: 'Build',
+ metadata: {
+ labels: {},
+ annotations: {
+ languageType,
+ },
+ name: '',
+ namespace,
+ },
+ spec: {
+ strategy: {
+ kind: 'ClusterBuildStrategy',
+ },
+ },
+ })
+
+ get mapper() {
+ return item => {
+ return {
+ ...ObjectMapper.default(item),
+ type: get(item, 'metadata.annotations.languageType'),
+ status: get(item, 'status.reason'),
+ }
+ }
+ }
+
+ getS2iSupportLanguage = () => {
+ return { s2i: ['java', 'nodejs', 'python', 'go'] }
+ }
+
+ getBuilderTemplate = async params =>
+ await this.stategies.fetchListByK8s(params)
+
+ @action
+ async create(data, params = {}) {
+ const res = await this.submitting(
+ request.post(this.getListUrl(params), data)
+ )
+ if (this.afterChange) {
+ this.afterChange(res)
+ }
+ return res
+ }
+
+ @action
+ run = params => {
+ return this.submitting(
+ request.post(`${this.getDetailUrl(params)}/imagebuildRuns`)
+ )
+ }
+
+ 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)
+ }
+ )
+ }
+}