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 ( +
+ {this.renderSupportTip()} + +
+

{t('IMAGE_FROM_S2I')}

+

{t('S2I_DESC')}

+
+
    + {this.state.s2i.map(type => ( +
  • + +

    {t(type.toUpperCase())}

    +
  • + ))} +
+ + + +
+ ) + } +} 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 ( +
+ +
+
+ + + + + +
+ {/*
+ + + +
*/} +
+ + + + +
+
+ + + +
+
+ +