diff --git a/frontend/packages/console-shared/src/components/formik-fields/RadioButtonField.tsx b/frontend/packages/console-shared/src/components/formik-fields/RadioButtonField.tsx index e37170595ba..f1b1c284236 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/RadioButtonField.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/RadioButtonField.tsx @@ -40,6 +40,7 @@ const RadioButtonField: React.FC = ({ label={option.label} isChecked={field.value === option.value} isValid={isValid} + isDisabled={option.isDisabled} aria-describedby={`${fieldId}-helper`} onChange={(val, event) => { field.onChange(event); diff --git a/frontend/packages/console-shared/src/components/formik-fields/field-types.ts b/frontend/packages/console-shared/src/components/formik-fields/field-types.ts index 89599e08d90..7eab0c04844 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/field-types.ts +++ b/frontend/packages/console-shared/src/components/formik-fields/field-types.ts @@ -94,6 +94,7 @@ export interface RadioButtonProps extends FieldProps { export interface RadioOption { value: string; label: React.ReactNode; + isDisabled?: boolean; children?: React.ReactNode; activeChildren?: React.ReactElement; } diff --git a/frontend/packages/dev-console/src/actions/modify-application.ts b/frontend/packages/dev-console/src/actions/modify-application.ts index 555782906c9..ffeb53b3f9f 100644 --- a/frontend/packages/dev-console/src/actions/modify-application.ts +++ b/frontend/packages/dev-console/src/actions/modify-application.ts @@ -1,6 +1,6 @@ import { KebabOption } from '@console/internal/components/utils'; import { K8sResourceKind, K8sKind } from '@console/internal/module/k8s'; -import { editApplicationModal, editApplication } from '../components/modals'; +import { editApplicationModal } from '../components/modals'; export const ModifyApplication = (kind: K8sKind, obj: K8sResourceKind): KebabOption => { return { @@ -24,8 +24,8 @@ export const ModifyApplication = (kind: K8sKind, obj: K8sResourceKind): KebabOpt export const EditApplication = (model: K8sKind, obj: K8sResourceKind): KebabOption => { return { - label: 'Edit Application', - callback: () => editApplication({ editAppResource: obj }), + label: 'Edit', + href: `/edit?name=${obj.metadata.name}&kind=${obj.kind}`, accessReview: { group: model.apiGroup, resource: model.plural, diff --git a/frontend/packages/dev-console/src/components/edit-application/EditApplication.tsx b/frontend/packages/dev-console/src/components/edit-application/EditApplication.tsx index 1acb51cb75b..8432ea0a039 100644 --- a/frontend/packages/dev-console/src/components/edit-application/EditApplication.tsx +++ b/frontend/packages/dev-console/src/components/edit-application/EditApplication.tsx @@ -1,93 +1,58 @@ import * as React from 'react'; import { Formik } from 'formik'; import * as _ from 'lodash'; +import { connect } from 'react-redux'; +import { getActivePerspective } from '@console/internal/reducers/ui'; +import { RootState } from '@console/internal/redux'; +import { history } from '@console/internal/components/utils'; import { NormalizedBuilderImages, normalizeBuilderImages } from '../../utils/imagestream-utils'; -import { createOrUpdateResources } from '../import/import-submit-utils'; -import { validationSchema } from '../import/import-validation-utils'; +import { + createOrUpdateResources as createOrUpdateGitResources, + handleRedirect, +} from '../import/import-submit-utils'; +import { validationSchema as gitValidationSchema } from '../import/import-validation-utils'; +import { createOrUpdateDeployImageResources } from '../import/deployImage-submit-utils'; +import { deployValidationSchema } from '../import/deployImage-validation-utils'; import EditApplicationForm from './EditApplicationForm'; import { EditApplicationProps } from './edit-application-types'; -import * as EditApplicationUtils from './edit-application-utils'; +import { getPageHeading, getInitialValues } from './edit-application-utils'; -const EditApplication: React.FC = ({ +export interface StateProps { + perspective: string; +} + +const EditApplication: React.FC = ({ + perspective, namespace, appName, - editAppResource, resources: appResources, - onCancel, - onSubmit, }) => { - const builderImages: NormalizedBuilderImages = + const imageStreamsData = appResources.imageStreams && appResources.imageStreams.loaded - ? normalizeBuilderImages(appResources.imageStreams.data) - : null; - - const currentImage = _.split( - _.get(appResources, 'buildConfig.data.spec.strategy.sourceStrategy.from.name', ''), - ':', - ); + ? appResources.imageStreams.data + : []; + const builderImages: NormalizedBuilderImages = !_.isEmpty(imageStreamsData) + ? normalizeBuilderImages(imageStreamsData) + : null; - const appGroupName = _.get(editAppResource, 'metadata.labels["app.kubernetes.io/part-of"]'); + const initialValues = getInitialValues(appResources, appName, namespace); + const pageHeading = getPageHeading(_.get(initialValues, 'build.strategy', '')); - const initialValues = { - formType: 'edit', - name: appName, - application: { - name: appGroupName, - selectedKey: appGroupName, - }, - project: { - name: namespace, - }, - git: EditApplicationUtils.getGitData(_.get(appResources, 'buildConfig.data')), - docker: { - dockerfilePath: _.get( - appResources, - 'buildConfig.data.spec.strategy.dockerStrategy.dockerfilePath', - 'Dockerfile', - ), - containerPort: parseInt( - _.split(_.get(appResources, 'route.data.spec.port.targetPort'), '-')[0], - 10, - ), - }, - image: { - selected: currentImage[0] || '', - recommended: '', - tag: currentImage[1] || '', - tagObj: {}, - ports: [], - isRecommending: false, - couldNotRecommend: false, - }, - route: EditApplicationUtils.getRouteData(_.get(appResources, 'route.data'), editAppResource), - resources: EditApplicationUtils.getResourcesType(editAppResource), - serverless: EditApplicationUtils.getServerlessData(editAppResource), - pipeline: { - enabled: false, - }, - build: EditApplicationUtils.getBuildData(_.get(appResources, 'buildConfig.data')), - deployment: EditApplicationUtils.getDeploymentData(editAppResource), - labels: EditApplicationUtils.getUserLabels(editAppResource), - limits: EditApplicationUtils.getLimitsData(editAppResource), + const updateResources = (values) => { + if (values.build.strategy) { + const imageStream = + values.image.selected && builderImages ? builderImages[values.image.selected].obj : null; + return createOrUpdateGitResources(values, imageStream, false, false, 'update', appResources); + } + return createOrUpdateDeployImageResources(values, false, 'update', appResources); }; const handleSubmit = (values, actions) => { - const imageStream = - values.image.selected && builderImages ? builderImages[values.image.selected].obj : null; - - createOrUpdateResources( - values, - imageStream, - false, - false, - 'update', - appResources, - editAppResource, - ) + updateResources(values) .then(() => { actions.setSubmitting(false); actions.setStatus({ error: '' }); - onSubmit(); + handleRedirect(namespace, perspective); }) .catch((err) => { actions.setSubmitting(false); @@ -96,18 +61,34 @@ const EditApplication: React.FC = ({ }; const renderForm = (props) => { - return ; + return ( + + ); }; return ( ); }; -export default EditApplication; +const mapStateToProps = (state: RootState) => { + const perspective = getActivePerspective(state); + return { + perspective, + }; +}; + +export default connect(mapStateToProps)(EditApplication); diff --git a/frontend/packages/dev-console/src/components/edit-application/EditApplicationForm.tsx b/frontend/packages/dev-console/src/components/edit-application/EditApplicationForm.tsx index 7ab1f8c91c6..95d92293623 100644 --- a/frontend/packages/dev-console/src/components/edit-application/EditApplicationForm.tsx +++ b/frontend/packages/dev-console/src/components/edit-application/EditApplicationForm.tsx @@ -1,16 +1,20 @@ import * as React from 'react'; import * as _ from 'lodash'; import { FormikProps, FormikValues } from 'formik'; -import { ModalTitle, ModalBody, ModalSubmitFooter } from '@console/internal/components/factory'; -import { BuildStrategyType } from '@console/internal/components/build'; +import { Form } from '@patternfly/react-core'; +import { PageHeading } from '@console/internal/components/utils'; +import { FormFooter } from '@console/shared'; import GitSection from '../import/git/GitSection'; import BuilderSection from '../import/builder/BuilderSection'; import DockerSection from '../import/git/DockerSection'; import AdvancedSection from '../import/advanced/AdvancedSection'; import AppSection from '../import/app/AppSection'; import { NormalizedBuilderImages } from '../../utils/imagestream-utils'; +import ImageSearchSection from '../import/image-search/ImageSearchSection'; +import { CreateApplicationFlow } from './edit-application-utils'; export interface EditApplicationFormProps { + createFlowType: string; builderImages?: NormalizedBuilderImages; } @@ -18,33 +22,36 @@ const EditApplicationForm: React.FC & EditApplicationF handleSubmit, handleReset, values, + createFlowType, builderImages, dirty, errors, status, isSubmitting, }) => ( -
- Edit Application - - {!_.isEmpty(values.build.strategy) && } - {values.build.strategy === BuildStrategyType.Source && ( + <> + + + {createFlowType !== CreateApplicationFlow.Container && } + {createFlowType === CreateApplicationFlow.Git && ( )} - {values.build.strategy === BuildStrategyType.Docker && ( + {createFlowType === CreateApplicationFlow.Dockerfile && ( )} + {createFlowType === CreateApplicationFlow.Container && } - - - + + + ); export default EditApplicationForm; diff --git a/frontend/packages/dev-console/src/components/edit-application/EditApplicationPage.tsx b/frontend/packages/dev-console/src/components/edit-application/EditApplicationPage.tsx new file mode 100644 index 00000000000..e22328a1b86 --- /dev/null +++ b/frontend/packages/dev-console/src/components/edit-application/EditApplicationPage.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import { Firehose, FirehoseResource, LoadingBox } from '@console/internal/components/utils'; +import { ImageStreamModel } from '@console/internal/models'; +import { RouteComponentProps } from 'react-router-dom'; +import { Helmet } from 'react-helmet'; +import { ServiceModel } from '@console/knative-plugin'; +import { referenceForModel } from '@console/internal/module/k8s'; +import NamespacedPage, { NamespacedPageVariants } from '../NamespacedPage'; +import EditApplication from './EditApplication'; +import { EditApplicationProps } from './edit-application-types'; + +const EditApplicationComponentLoader: React.FunctionComponent = ( + props: EditApplicationProps, +) => { + const { loaded } = props; + return loaded ? : ; +}; + +export type ImportPageProps = RouteComponentProps<{ ns?: string }>; + +const EditApplicationPage: React.FunctionComponent = ({ match, location }) => { + const namespace = match.params.ns; + const queryParams = new URLSearchParams(location.search); + const editAppResourceKind = queryParams.get('kind'); + const appName = queryParams.get('name'); + const appResources: FirehoseResource[] = [ + { + kind: 'Service', + prop: 'service', + name: appName, + namespace, + optional: true, + }, + { + kind: 'BuildConfig', + prop: 'buildConfig', + name: appName, + namespace, + optional: true, + }, + { + kind: 'Route', + prop: 'route', + name: appName, + namespace, + optional: true, + }, + { + kind: 'ImageStream', + prop: 'imageStream', + name: appName, + namespace, + optional: true, + }, + { + kind: ImageStreamModel.kind, + prop: 'imageStreams', + isList: true, + namespace: 'openshift', + optional: true, + }, + ]; + let kind = editAppResourceKind; + if (kind === ServiceModel.kind) { + kind = referenceForModel(ServiceModel); + } + appResources.push({ + kind, + prop: 'editAppResource', + name: appName, + namespace, + optional: true, + }); + + return ( + + + Edit + +
+ + + +
+
+ ); +}; + +export default EditApplicationPage; diff --git a/frontend/packages/dev-console/src/components/edit-application/EditApplicationWrapper.tsx b/frontend/packages/dev-console/src/components/edit-application/EditApplicationWrapper.tsx deleted file mode 100644 index 4f4196bc700..00000000000 --- a/frontend/packages/dev-console/src/components/edit-application/EditApplicationWrapper.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { K8sResourceKind } from '@console/internal/module/k8s'; -import * as React from 'react'; -import { Firehose, FirehoseResource } from '@console/internal/components/utils'; -import { ImageStreamModel } from '@console/internal/models'; -import { createModalLauncher, ModalComponentProps } from '@console/internal/components/factory'; -import EditApplication from './EditApplication'; -import { EditApplicationProps } from './edit-application-types'; - -interface EditApplicationWrapperProps { - editAppResource: K8sResourceKind; -} - -type Props = EditApplicationWrapperProps & ModalComponentProps; - -const EditApplicationComponentLoader: React.FunctionComponent = ( - props: EditApplicationProps, -) => { - const { loaded } = props; - return loaded && ; -}; - -const EditApplicationWrapper: React.FunctionComponent = ({ - editAppResource, - cancel, - close, -}) => { - const { name, namespace } = editAppResource.metadata; - const appResources: FirehoseResource[] = [ - { - kind: 'Service', - prop: 'service', - name, - namespace, - optional: true, - }, - { - kind: 'BuildConfig', - prop: 'buildConfig', - name, - namespace, - optional: true, - }, - { - kind: 'Route', - prop: 'route', - name, - namespace, - optional: true, - }, - { - kind: ImageStreamModel.kind, - prop: 'imageStreams', - isList: true, - namespace: 'openshift', - optional: true, - }, - ]; - - return ( - - - - ); -}; - -export const editApplication = createModalLauncher((props: Props) => ( - -)); - -export default EditApplicationWrapper; diff --git a/frontend/packages/dev-console/src/components/edit-application/__tests__/edit-application-data.ts b/frontend/packages/dev-console/src/components/edit-application/__tests__/edit-application-data.ts new file mode 100644 index 00000000000..f0f35b35bc7 --- /dev/null +++ b/frontend/packages/dev-console/src/components/edit-application/__tests__/edit-application-data.ts @@ -0,0 +1,585 @@ +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { ServiceModel } from '@console/knative-plugin'; +import { AppResources } from '../edit-application-types'; +import { GitImportFormData, Resources, DeployImageFormData } from '../../import/import-types'; +import { UNASSIGNED_KEY } from '../../import/app/ApplicationSelector'; + +export const knativeService: K8sResourceKind = { + apiVersion: `${ServiceModel.apiGroup}/${ServiceModel.apiVersion}`, + kind: `${ServiceModel.kind}`, + metadata: { + name: 'sample', + namespace: 'div', + }, + spec: { + template: { + spec: { + containers: [ + { + image: 'openshift/hello-openshift', + }, + ], + }, + }, + }, +}; + +export const appResources: AppResources = { + editAppResource: { + loaded: true, + loadError: '', + data: { + kind: 'DeploymentConfig', + apiVersion: 'apps.openshift.io/v1', + metadata: { + annotations: { + 'app.openshift.io/vcs-ref': 'master', + 'app.openshift.io/vcs-uri': 'https://github.com/divyanshiGupta/nationalparks-py', + }, + selfLink: '/apis/apps.openshift.io/v1/namespaces/div/deploymentconfigs/nationalparks-py', + resourceVersion: '329826', + name: 'nationalparks-py', + uid: 'c3f2b32d-d5fd-4050-9735-0c95828af6fd', + creationTimestamp: '2020-01-13T10:33:05Z', + generation: 16, + namespace: 'div', + labels: { + app: 'nationalparks-py', + 'app.kubernetes.io/component': 'nationalparks-py', + 'app.kubernetes.io/instance': 'nationalparks-py', + 'app.kubernetes.io/name': 'python', + 'app.openshift.io/runtime': 'python', + 'app.openshift.io/runtime-version': '3.6', + 'app.openshift.io/runtime-namespace': 'div', + }, + }, + spec: { + strategy: { + type: 'Rolling', + rollingParams: { + updatePeriodSeconds: 1, + intervalSeconds: 1, + timeoutSeconds: 600, + maxUnavailable: '25%', + maxSurge: '25%', + }, + resources: {}, + activeDeadlineSeconds: 21600, + }, + triggers: [ + { + type: 'ImageChange', + imageChangeParams: { + automatic: true, + containerNames: ['nationalparks-py'], + from: { + kind: 'ImageStreamTag', + namespace: 'div', + name: 'nationalparks-py:latest', + }, + lastTriggeredImage: + 'image-registry.openshift-image-registry.svc:5000/div/nationalparks-py@sha256:7d67c08b5b993d72533f9bb07b6429c5a2263de8b67cc1b0ae09d4c0b0d39f97', + }, + }, + { + type: 'ConfigChange', + }, + ], + replicas: 1, + revisionHistoryLimit: 10, + test: false, + selector: { + app: 'nationalparks-py', + deploymentconfig: 'nationalparks-py', + }, + template: { + metadata: { + creationTimestamp: null, + labels: { + app: 'nationalparks-py', + deploymentconfig: 'nationalparks-py', + }, + }, + spec: { + containers: [ + { + name: 'nationalparks-py', + image: + 'image-registry.openshift-image-registry.svc:5000/div/nationalparks-py@sha256:7d67c08b5b993d72533f9bb07b6429c5a2263de8b67cc1b0ae09d4c0b0d39f97', + ports: [ + { + containerPort: 8080, + protocol: 'TCP', + }, + ], + resources: {}, + terminationMessagePath: '/dev/termination-log', + terminationMessagePolicy: 'File', + imagePullPolicy: 'Always', + }, + ], + restartPolicy: 'Always', + terminationGracePeriodSeconds: 30, + dnsPolicy: 'ClusterFirst', + securityContext: {}, + schedulerName: 'default-scheduler', + }, + }, + }, + status: { + observedGeneration: 16, + details: { + message: 'image change', + causes: [ + { + type: 'ImageChange', + imageTrigger: { + from: { + kind: 'DockerImage', + name: + 'image-registry.openshift-image-registry.svc:5000/div/nationalparks-py@sha256:7d67c08b5b993d72533f9bb07b6429c5a2263de8b67cc1b0ae09d4c0b0d39f97', + }, + }, + }, + ], + }, + availableReplicas: 1, + unavailableReplicas: 0, + latestVersion: 7, + updatedReplicas: 1, + conditions: [ + { + type: 'Available', + status: 'True', + lastUpdateTime: '2020-01-13T10:35:13Z', + lastTransitionTime: '2020-01-13T10:35:13Z', + message: 'Deployment config has minimum availability.', + }, + { + type: 'Progressing', + status: 'True', + lastUpdateTime: '2020-01-13T17:59:39Z', + lastTransitionTime: '2020-01-13T17:59:36Z', + reason: 'NewReplicationControllerAvailable', + message: 'replication controller "nationalparks-py-7" successfully rolled out', + }, + ], + replicas: 1, + readyReplicas: 1, + }, + }, + }, + route: { + loaded: true, + loadError: '', + data: { + kind: 'Route', + apiVersion: 'route.openshift.io/v1', + metadata: { + name: 'nationalparks-py', + namespace: 'div', + selfLink: '/apis/route.openshift.io/v1/namespaces/div/routes/nationalparks-py', + uid: 'e9f365ed-5c67-40b2-ab0b-b38544f943d3', + resourceVersion: '329838', + creationTimestamp: '2020-01-13T10:33:05Z', + labels: { + app: 'nationalparks-py', + 'app.kubernetes.io/component': 'nationalparks-py', + 'app.kubernetes.io/instance': 'nationalparks-py', + 'app.kubernetes.io/name': 'python', + 'app.openshift.io/runtime': 'python', + 'app.openshift.io/runtime-version': '3.6', + }, + annotations: { + 'openshift.io/host.generated': 'true', + }, + }, + spec: { + host: 'nationalparks-py-div.apps.rorai-cluster34.devcluster.openshift.com', + to: { + kind: 'Service', + name: 'nationalparks-py', + weight: 100, + }, + port: { + targetPort: '8080-tcp', + }, + wildcardPolicy: 'None', + }, + status: { + ingress: [ + { + host: 'nationalparks-py-div.apps.rorai-cluster34.devcluster.openshift.com', + routerName: 'default', + conditions: [ + { + type: 'Admitted', + status: 'True', + lastTransitionTime: '2020-01-13T10:33:06Z', + }, + ], + wildcardPolicy: 'None', + routerCanonicalHostname: 'apps.rorai-cluster34.devcluster.openshift.com', + }, + ], + }, + }, + }, + buildConfig: { + loaded: true, + loadError: '', + data: { + kind: 'BuildConfig', + apiVersion: 'build.openshift.io/v1', + metadata: { + name: 'nationalparks-py', + namespace: 'div', + selfLink: '/apis/build.openshift.io/v1/namespaces/div/buildconfigs/nationalparks-py', + uid: '8319b0c9-3674-4eb6-b0bb-3bcfc7211435', + resourceVersion: '329844', + creationTimestamp: '2020-01-13T10:33:05Z', + labels: { + app: 'nationalparks-py', + 'app.kubernetes.io/component': 'nationalparks-py', + 'app.kubernetes.io/instance': 'nationalparks-py', + 'app.kubernetes.io/name': 'python', + 'app.openshift.io/runtime': 'python', + 'app.openshift.io/runtime-version': '3.6', + }, + annotations: { + 'app.openshift.io/vcs-ref': 'master', + 'app.openshift.io/vcs-uri': 'https://github.com/divyanshiGupta/nationalparks-py', + }, + }, + spec: { + nodeSelector: null, + output: { + to: { + kind: 'ImageStreamTag', + name: 'nationalparks-py:latest', + }, + }, + resources: {}, + successfulBuildsHistoryLimit: 5, + failedBuildsHistoryLimit: 5, + strategy: { + type: 'Source', + sourceStrategy: { + from: { + kind: 'ImageStreamTag', + namespace: 'openshift', + name: 'python:3.6', + }, + }, + }, + postCommit: {}, + source: { + type: 'Git', + git: { + uri: 'https://github.com/divyanshiGupta/nationalparks-py', + }, + contextDir: '/', + }, + triggers: [ + { + type: 'Generic', + generic: { + secretReference: { + name: 'nationalparks-py-generic-webhook-secret', + }, + }, + }, + { + type: 'GitHub', + github: { + secretReference: { + name: 'nationalparks-py-github-webhook-secret', + }, + }, + }, + { + type: 'ImageChange', + imageChange: { + lastTriggeredImageID: + 'image-registry.openshift-image-registry.svc:5000/openshift/python@sha256:dde8883b3033d9b3d0e88b8c74304fba4b23cd5c07a164e71ee352b899a7803e', + }, + }, + { + type: 'ConfigChange', + }, + ], + runPolicy: 'Serial', + }, + status: { + lastVersion: 3, + }, + }, + }, + imageStream: { + loaded: true, + loadError: '', + data: { + kind: 'ImageStream', + apiVersion: 'image.openshift.io/v1', + metadata: { + annotations: { + 'app.openshift.io/vcs-ref': 'master', + 'app.openshift.io/vcs-uri': 'https://github.com/divyanshiGupta/nationalparks-py', + }, + selfLink: '/apis/image.openshift.io/v1/namespaces/div/imagestreams/nationalparks-py', + resourceVersion: '676247', + name: 'nationalparks-py', + uid: '2bc985ac-f834-45e5-9a86-830edb6bc8bd', + creationTimestamp: '2020-01-15T15:51:47Z', + generation: 1, + namespace: 'div', + labels: { + app: 'nationalparks-py', + 'app.kubernetes.io/component': 'nationalparks-py', + 'app.kubernetes.io/instance': 'nationalparks-py', + 'app.kubernetes.io/name': 'python', + 'app.kubernetes.io/part-of': 'nodejs-rest-http-app', + 'app.openshift.io/runtime': 'python', + 'app.openshift.io/runtime-version': '3.6', + }, + }, + spec: { + lookupPolicy: { + local: false, + }, + }, + status: { + dockerImageRepository: + 'image-registry.openshift-image-registry.svc:5000/div/nationalparks-py', + }, + }, + }, +}; + +export const gitImportInitialValues: GitImportFormData = { + formType: 'edit', + name: 'nationalparks-py', + application: { name: '', selectedKey: UNASSIGNED_KEY }, + project: { name: 'div' }, + route: { + disable: true, + create: true, + targetPort: '8080-tcp', + unknownTargetPort: '', + defaultUnknownPort: 8080, + path: '', + hostname: 'nationalparks-py-div.apps.rorai-cluster34.devcluster.openshift.com', + secure: false, + tls: { + termination: '', + insecureEdgeTerminationPolicy: '', + caCertificate: '', + certificate: '', + destinationCACertificate: '', + privateKey: '', + }, + }, + resources: Resources.OpenShift, + serverless: { + scaling: { + minpods: 0, + maxpods: '', + concurrencytarget: '', + concurrencylimit: '', + }, + }, + pipeline: { enabled: false }, + deployment: { env: [], triggers: { image: true, config: true }, replicas: 1 }, + labels: {}, + limits: { + cpu: { + request: '', + requestUnit: '', + defaultRequestUnit: '', + limit: '', + limitUnit: '', + defaultLimitUnit: '', + }, + memory: { + request: '', + requestUnit: 'Mi', + defaultRequestUnit: 'Mi', + limit: '', + limitUnit: 'Mi', + defaultLimitUnit: 'Mi', + }, + }, + git: { + url: 'https://github.com/divyanshiGupta/nationalparks-py', + type: 'Git', + ref: '', + dir: '/', + showGitType: false, + secret: '', + isUrlValidated: false, + isUrlValidating: false, + }, + docker: { dockerfilePath: 'Dockerfile', containerPort: 8080 }, + image: { + selected: 'python', + recommended: '', + tag: '3.6', + tagObj: {}, + ports: [], + isRecommending: false, + couldNotRecommend: false, + }, + build: { + env: [], + triggers: { webhook: true, image: true, config: true }, + strategy: 'Source', + }, +}; + +export const externalImageValues: DeployImageFormData = { + formType: 'edit', + name: 'nationalparks-py', + application: { name: '', selectedKey: '#UNASSIGNED_KEY#' }, + project: { name: 'div' }, + route: { + disable: true, + create: true, + targetPort: '8080-tcp', + unknownTargetPort: '', + defaultUnknownPort: 8080, + path: '', + hostname: 'nationalparks-py-div.apps.rorai-cluster34.devcluster.openshift.com', + secure: false, + tls: { + termination: '', + insecureEdgeTerminationPolicy: '', + caCertificate: '', + certificate: '', + destinationCACertificate: '', + privateKey: '', + }, + }, + resources: Resources.OpenShift, + serverless: { + scaling: { + minpods: 0, + maxpods: '', + concurrencytarget: '', + concurrencylimit: '', + }, + }, + pipeline: { enabled: false }, + deployment: { env: [], triggers: { image: true, config: true }, replicas: 1 }, + labels: {}, + limits: { + cpu: { + request: '', + requestUnit: '', + defaultRequestUnit: '', + limit: '', + limitUnit: '', + defaultLimitUnit: '', + }, + memory: { + request: '', + requestUnit: 'Mi', + defaultRequestUnit: 'Mi', + limit: '', + limitUnit: 'Mi', + defaultLimitUnit: 'Mi', + }, + }, + searchTerm: undefined, + registry: 'external', + imageStream: { image: '', tag: '', namespace: '', grantAccess: true }, + isi: { + name: '', + image: {}, + tag: '', + status: { metadata: {}, status: '' }, + ports: [], + }, + image: { + name: '', + image: {}, + tag: '', + status: { metadata: {}, status: '' }, + ports: [], + }, + build: { env: [], triggers: {}, strategy: '' }, + isSearchingForImage: false, +}; + +export const internalImageValues: DeployImageFormData = { + formType: 'edit', + name: 'nationalparks-py', + application: { name: '', selectedKey: '#UNASSIGNED_KEY#' }, + project: { name: 'div' }, + route: { + disable: true, + create: true, + targetPort: '8080-tcp', + unknownTargetPort: '', + defaultUnknownPort: 8080, + path: '', + hostname: 'nationalparks-py-div.apps.rorai-cluster34.devcluster.openshift.com', + secure: false, + tls: { + termination: '', + insecureEdgeTerminationPolicy: '', + caCertificate: '', + certificate: '', + destinationCACertificate: '', + privateKey: '', + }, + }, + resources: Resources.OpenShift, + serverless: { + scaling: { + minpods: 0, + maxpods: '', + concurrencytarget: '', + concurrencylimit: '', + }, + }, + pipeline: { enabled: false }, + deployment: { env: [], triggers: { image: true, config: true }, replicas: 1 }, + labels: {}, + limits: { + cpu: { + request: '', + requestUnit: '', + defaultRequestUnit: '', + limit: '', + limitUnit: '', + defaultLimitUnit: '', + }, + memory: { + request: '', + requestUnit: 'Mi', + defaultRequestUnit: 'Mi', + limit: '', + limitUnit: 'Mi', + defaultLimitUnit: 'Mi', + }, + }, + searchTerm: '', + registry: 'internal', + imageStream: { image: 'python', tag: '3.6', namespace: 'div' }, + isi: { + name: '', + image: {}, + tag: '', + status: { metadata: {}, status: '' }, + ports: [], + }, + image: { + name: '', + image: {}, + tag: '', + status: { metadata: {}, status: '' }, + ports: [], + }, + build: { env: [], triggers: {}, strategy: '' }, + isSearchingForImage: false, +}; diff --git a/frontend/packages/dev-console/src/components/edit-application/__tests__/edit-application-utils.spec.ts b/frontend/packages/dev-console/src/components/edit-application/__tests__/edit-application-utils.spec.ts new file mode 100644 index 00000000000..e47c7b39911 --- /dev/null +++ b/frontend/packages/dev-console/src/components/edit-application/__tests__/edit-application-utils.spec.ts @@ -0,0 +1,38 @@ +import { BuildStrategyType } from '@console/internal/components/build'; +import { + getResourcesType, + getPageHeading, + CreateApplicationFlow, + getInitialValues, +} from '../edit-application-utils'; +import { Resources } from '../../import/import-types'; +import { + knativeService, + appResources, + gitImportInitialValues, + externalImageValues, + internalImageValues, +} from './edit-application-data'; + +describe('Edit Application Utils', () => { + it('getResourcesType should return resource type based on resource kind', () => { + expect(getResourcesType(knativeService)).toEqual(Resources.KnativeService); + }); + + it('getPageHeading should return page heading based on the create flow used to create the application', () => { + expect(getPageHeading(BuildStrategyType.Source)).toEqual(CreateApplicationFlow.Git); + }); + + it('getInitialValues should return values based on the resources and the create flow used to create the application', () => { + const { route, editAppResource, buildConfig, imageStream } = appResources; + expect( + getInitialValues({ buildConfig, editAppResource, route }, 'nationalparks-py', 'div'), + ).toEqual(gitImportInitialValues); + expect( + getInitialValues({ editAppResource, route, imageStream }, 'nationalparks-py', 'div'), + ).toEqual(externalImageValues); + expect(getInitialValues({ editAppResource, route }, 'nationalparks-py', 'div')).toEqual( + internalImageValues, + ); + }); +}); diff --git a/frontend/packages/dev-console/src/components/edit-application/edit-application-types.ts b/frontend/packages/dev-console/src/components/edit-application/edit-application-types.ts index 2028d5f3218..a60481e2a64 100644 --- a/frontend/packages/dev-console/src/components/edit-application/edit-application-types.ts +++ b/frontend/packages/dev-console/src/components/edit-application/edit-application-types.ts @@ -2,18 +2,17 @@ import { K8sResourceKind } from '@console/internal/module/k8s'; import { FirehoseResult } from '@console/internal/components/utils'; export interface AppResources { - service?: FirehoseResult; - route?: FirehoseResult; - buildConfig?: FirehoseResult; + service?: FirehoseResult; + route?: FirehoseResult; + buildConfig?: FirehoseResult; + imageStream?: FirehoseResult; + editAppResource?: FirehoseResult; imageStreams?: FirehoseResult; } export interface EditApplicationProps { namespace: string; appName: string; - editAppResource: K8sResourceKind; resources?: AppResources; loaded?: boolean; - onCancel?: () => void; - onSubmit?: () => void; } diff --git a/frontend/packages/dev-console/src/components/edit-application/edit-application-utils.ts b/frontend/packages/dev-console/src/components/edit-application/edit-application-utils.ts index cd64f30c321..6f532386ff6 100644 --- a/frontend/packages/dev-console/src/components/edit-application/edit-application-utils.ts +++ b/frontend/packages/dev-console/src/components/edit-application/edit-application-utils.ts @@ -4,6 +4,14 @@ import { BuildStrategyType } from '@console/internal/components/build'; import { DeploymentConfigModel, DeploymentModel } from '@console/internal/models'; import { ServiceModel } from '@console/knative-plugin'; import { Resources } from '../import/import-types'; +import { UNASSIGNED_KEY } from '../import/app/ApplicationSelector'; +import { AppResources } from './edit-application-types'; + +export enum CreateApplicationFlow { + Git = 'Import from Git', + Dockerfile = 'Import from Dockerfile', + Container = 'Deploy Image', +} export const getResourcesType = (resource: K8sResourceKind): string => { switch (resource.kind) { @@ -18,6 +26,17 @@ export const getResourcesType = (resource: K8sResourceKind): string => { } }; +export const getPageHeading = (buildStrategy: string): string => { + switch (buildStrategy) { + case BuildStrategyType.Source: + return CreateApplicationFlow.Git; + case BuildStrategyType.Docker: + return CreateApplicationFlow.Dockerfile; + default: + return CreateApplicationFlow.Container; + } +}; + const checkIfTriggerExists = (triggers: { [key: string]: string | {} }[], type: string) => { return !!_.find(triggers, (trigger) => { return trigger.type === type; @@ -40,10 +59,11 @@ export const getGitData = (buildConfig: K8sResourceKind) => { export const getRouteData = (route: K8sResourceKind, resource: K8sResourceKind) => { let routeData = { - show: _.isEmpty(route), + disable: !_.isEmpty(route), create: true, targetPort: _.get(route, 'spec.port.targetPort', ''), unknownTargetPort: '', + defaultUnknownPort: 8080, path: _.get(route, 'spec.path', ''), hostname: _.get(route, 'spec.host', ''), secure: _.has(route, 'spec.termination'), @@ -61,8 +81,8 @@ export const getRouteData = (route: K8sResourceKind, resource: K8sResourceKind) const port = _.get(containers[0], 'ports[0].containerPort', ''); routeData = { ...routeData, - show: - _.get(resource, 'metadata.labels["serving.knative.dev/visibility"]', '') === + disable: + _.get(resource, 'metadata.labels["serving.knative.dev/visibility"]', '') !== 'cluster-local', unknownTargetPort: _.toString(port), }; @@ -85,7 +105,7 @@ export const getBuildData = (buildConfig: K8sResourceKind) => { } const triggers = _.get(buildConfig, 'spec.triggers'); const buildData = { - env: buildStrategyData.env, + env: buildStrategyData.env || [], triggers: { webhook: checkIfTriggerExists(triggers, 'GitHub'), image: checkIfTriggerExists(triggers, 'ImageChange'), @@ -97,7 +117,14 @@ export const getBuildData = (buildConfig: K8sResourceKind) => { }; export const getServerlessData = (resource: K8sResourceKind) => { - let serverlessData = {}; + let serverlessData = { + scaling: { + minpods: 0, + maxpods: '', + concurrencytarget: '', + concurrencylimit: '', + }, + }; if (getResourcesType(resource) === Resources.KnativeService) { const annotations = _.get(resource, 'spec.template.metadata.annotations'); serverlessData = { @@ -166,8 +193,189 @@ export const getUserLabels = (resource: K8sResourceKind) => { 'app.openshift.io/runtime', 'app.kubernetes.io/part-of', 'app.openshift.io/runtime-version', + 'app.openshift.io/runtime-namespace', ]; const allLabels = _.get(resource, 'metadata.labels', {}); const userLabels = _.omit(allLabels, defaultLabels); return userLabels; }; + +export const getCommonInitialValues = ( + editAppResource: K8sResourceKind, + route: K8sResourceKind, + name: string, + namespace: string, +) => { + const appGroupName = _.get(editAppResource, 'metadata.labels["app.kubernetes.io/part-of"]'); + const commonInitialValues = { + formType: 'edit', + name, + application: { + name: appGroupName || '', + selectedKey: appGroupName || UNASSIGNED_KEY, + }, + project: { + name: namespace, + }, + route: getRouteData(route, editAppResource), + resources: getResourcesType(editAppResource), + serverless: getServerlessData(editAppResource), + pipeline: { + enabled: false, + }, + deployment: getDeploymentData(editAppResource), + labels: getUserLabels(editAppResource), + limits: getLimitsData(editAppResource), + }; + return commonInitialValues; +}; + +export const getGitAndDockerfileInitialValues = ( + buildConfig: K8sResourceKind, + route: K8sResourceKind, +) => { + if (_.isEmpty(buildConfig)) { + return {}; + } + const currentImage = _.split( + _.get(buildConfig, 'spec.strategy.sourceStrategy.from.name', ''), + ':', + ); + const initialValues = { + git: getGitData(buildConfig), + docker: { + dockerfilePath: _.get( + buildConfig, + 'spec.strategy.dockerStrategy.dockerfilePath', + 'Dockerfile', + ), + containerPort: parseInt(_.split(_.get(route, 'spec.port.targetPort'), '-')[0], 10), + }, + image: { + selected: currentImage[0] || '', + recommended: '', + tag: currentImage[1] || '', + tagObj: {}, + ports: [], + isRecommending: false, + couldNotRecommend: false, + }, + build: getBuildData(buildConfig), + }; + return initialValues; +}; + +export const getExternalImageInitialValues = (imageStream: K8sResourceKind) => { + if (_.isEmpty(imageStream)) { + return {}; + } + const name = _.get(imageStream, 'spec.tags[0].from.name'); + const deployImageInitialValues = { + searchTerm: name, + registry: 'external', + imageStream: { + image: '', + tag: '', + namespace: '', + grantAccess: true, + }, + isi: { + name: '', + image: {}, + tag: '', + status: { metadata: {}, status: '' }, + ports: [], + }, + image: { + name: '', + image: {}, + tag: '', + status: { metadata: {}, status: '' }, + ports: [], + }, + build: { + env: [], + triggers: {}, + strategy: '', + }, + isSearchingForImage: false, + }; + return deployImageInitialValues; +}; + +export const getInternalImageInitialValues = (editAppResource: K8sResourceKind) => { + const imageStreamNamespace = _.get( + editAppResource, + 'metadata.labels["app.openshift.io/runtime-namespace"]', + '', + ); + const imageStreamName = _.get(editAppResource, 'metadata.labels["app.openshift.io/runtime"]', ''); + const imageStreamTag = _.get( + editAppResource, + 'metadata.labels["app.openshift.io/runtime-version"]', + '', + ); + const deployImageInitialValues = { + searchTerm: '', + registry: 'internal', + imageStream: { + image: imageStreamName, + tag: imageStreamTag, + namespace: imageStreamNamespace, + }, + isi: { + name: '', + image: {}, + tag: '', + status: { metadata: {}, status: '' }, + ports: [], + }, + image: { + name: '', + image: {}, + tag: '', + status: { metadata: {}, status: '' }, + ports: [], + }, + build: { + env: [], + triggers: {}, + strategy: '', + }, + isSearchingForImage: false, + }; + return deployImageInitialValues; +}; + +export const getInitialValues = ( + appResources: AppResources, + appName: string, + namespace: string, +) => { + const commonValues = getCommonInitialValues( + _.get(appResources, 'editAppResource.data'), + _.get(appResources, 'route.data'), + appName, + namespace, + ); + const gitDockerValues = getGitAndDockerfileInitialValues( + _.get(appResources, 'buildConfig.data'), + _.get(appResources, 'route.data'), + ); + let externalImageValues = {}; + let internalImageValues = {}; + + if (_.isEmpty(gitDockerValues)) { + externalImageValues = getExternalImageInitialValues(_.get(appResources, 'imageStream.data')); + internalImageValues = _.isEmpty(externalImageValues) + ? getInternalImageInitialValues(_.get(appResources, 'editAppResource.data')) + : {}; + } + + return { + ...commonValues, + ...gitDockerValues, + ...externalImageValues, + ...internalImageValues, + }; +}; diff --git a/frontend/packages/dev-console/src/components/import/DeployImage.tsx b/frontend/packages/dev-console/src/components/import/DeployImage.tsx index 3f8a96b8391..a4516d75d38 100644 --- a/frontend/packages/dev-console/src/components/import/DeployImage.tsx +++ b/frontend/packages/dev-console/src/components/import/DeployImage.tsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import { ALL_APPLICATIONS_KEY } from '@console/shared'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { DeployImageFormData, FirehoseList, Resources } from './import-types'; -import { createResources } from './deployImage-submit-utils'; +import { createOrUpdateDeployImageResources } from './deployImage-submit-utils'; import { deployValidationSchema } from './deployImage-validation-utils'; import DeployImageForm from './DeployImageForm'; @@ -67,7 +67,7 @@ const DeployImage: React.FC = ({ namespace, projects, activeApplication } }, }, route: { - show: true, + disable: false, create: true, targetPort: '', unknownTargetPort: '', @@ -129,10 +129,13 @@ const DeployImage: React.FC = ({ namespace, projects, activeApplication } project: { name: projectName }, } = values; - const dryRunRequests: Promise = createResources(values, true); + const dryRunRequests: Promise = createOrUpdateDeployImageResources( + values, + true, + ); dryRunRequests .then(() => { - const requests: Promise = createResources(values); + const requests: Promise = createOrUpdateDeployImageResources(values); return requests; }) .then(() => { diff --git a/frontend/packages/dev-console/src/components/import/ImportForm.tsx b/frontend/packages/dev-console/src/components/import/ImportForm.tsx index 4e490c55ad6..714a44a3395 100644 --- a/frontend/packages/dev-console/src/components/import/ImportForm.tsx +++ b/frontend/packages/dev-console/src/components/import/ImportForm.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import * as plugins from '@console/internal/plugins'; import { Formik } from 'formik'; import * as _ from 'lodash'; import { history, AsyncComponent } from '@console/internal/components/utils'; @@ -9,7 +8,7 @@ import { connect } from 'react-redux'; import { ALL_APPLICATIONS_KEY } from '@console/shared'; import { NormalizedBuilderImages, normalizeBuilderImages } from '../../utils/imagestream-utils'; import { GitImportFormData, FirehoseList, ImportData, Resources } from './import-types'; -import { createOrUpdateResources } from './import-submit-utils'; +import { createOrUpdateResources, handleRedirect } from './import-submit-utils'; import { validationSchema } from './import-validation-utils'; export interface ImportFormProps { @@ -71,7 +70,7 @@ const ImportForm: React.FC = ({ couldNotRecommend: false, }, route: { - show: true, + disable: false, create: true, targetPort: '', path: '', @@ -138,14 +137,6 @@ const ImportForm: React.FC = ({ const builderImages: NormalizedBuilderImages = imageStreams && imageStreams.loaded && normalizeBuilderImages(imageStreams.data); - const handleRedirect = (project: string) => { - const perspectiveData = plugins.registry - .getPerspectives() - .find((item) => item.properties.id === perspective); - const redirectURL = perspectiveData.properties.getImportRedirectURL(project); - history.push(redirectURL); - }; - const handleSubmit = (values, actions) => { const imageStream = builderImages && builderImages[values.image.selected].obj; const createNewProject = projects.loaded && _.isEmpty(projects.data); @@ -157,7 +148,7 @@ const ImportForm: React.FC = ({ .then(() => createOrUpdateResources(values, imageStream)) .then(() => { actions.setSubmitting(false); - handleRedirect(projectName); + handleRedirect(projectName, perspective); }) .catch((err) => { actions.setSubmitting(false); diff --git a/frontend/packages/dev-console/src/components/import/__tests__/deployImage-submit-utils.spec.ts b/frontend/packages/dev-console/src/components/import/__tests__/deployImage-submit-utils.spec.ts index ca66f2404ba..7669647356f 100644 --- a/frontend/packages/dev-console/src/components/import/__tests__/deployImage-submit-utils.spec.ts +++ b/frontend/packages/dev-console/src/components/import/__tests__/deployImage-submit-utils.spec.ts @@ -18,7 +18,11 @@ import { internalImageData, } from './deployImage-submit-utils-data'; -const { ensurePortExists, createDeployment, createResources } = submitUtils; +const { + ensurePortExists, + createOrUpdateDeployment, + createOrUpdateDeployImageResources, +} = submitUtils; describe('DeployImage Submit Utils', () => { describe('Ensure Port Exists', () => { @@ -71,7 +75,7 @@ describe('DeployImage Submit Utils', () => { }); it('should choose image from dockerImageReference when creating Deployment using internal imagestream', (done) => { - createDeployment(internalImageData, false) + createOrUpdateDeployment(internalImageData, false) .then((returnValue) => { expect(_.get(returnValue, 'model.kind')).toEqual(DeploymentModel.kind); expect(_.get(returnValue, 'data.spec.template.spec.containers[0].image')).toEqual( @@ -105,7 +109,7 @@ describe('DeployImage Submit Utils', () => { }, }; - createDeployment(data, false) + createOrUpdateDeployment(data, false) .then((returnValue) => { expect(_.get(returnValue, 'data.spec.template.spec.containers[0].resources')).toEqual({ limits: { cpu: '10m', memory: '200Mi' }, @@ -131,7 +135,7 @@ describe('DeployImage Submit Utils', () => { }); it('should call createImageStream when creating Resources using external image', (done) => { - createResources(defaultData, false) + createOrUpdateDeployImageResources(defaultData, false) .then((returnValue) => { expect(returnValue).toHaveLength(4); const models = returnValue.map((data) => _.get(data, 'model.kind')); @@ -149,7 +153,7 @@ describe('DeployImage Submit Utils', () => { }); it('should not call createImageStream when creating Resources using internal imagestream', (done) => { - createResources(internalImageData, false) + createOrUpdateDeployImageResources(internalImageData, false) .then((returnValue) => { expect(returnValue).toHaveLength(3); const models = returnValue.map((data) => _.get(data, 'model.kind')); @@ -166,14 +170,14 @@ describe('DeployImage Submit Utils', () => { mockData.resources = Resources.KnativeService; const imageStreamSpy = jest - .spyOn(submitUtils, 'createImageStream') + .spyOn(submitUtils, 'createOrUpdateImageStream') .mockImplementation(() => ({ status: { dockerImageReference: 'test:1234', }, })); - createResources(mockData, false) + createOrUpdateDeployImageResources(mockData, false) .then((returnValue) => { // createImageStream is called as separate entity expect(imageStreamSpy).toHaveBeenCalled(); diff --git a/frontend/packages/dev-console/src/components/import/__tests__/import-submit-utils.spec.ts b/frontend/packages/dev-console/src/components/import/__tests__/import-submit-utils.spec.ts index 208dcca634e..bb0c61dcbe3 100644 --- a/frontend/packages/dev-console/src/components/import/__tests__/import-submit-utils.spec.ts +++ b/frontend/packages/dev-console/src/components/import/__tests__/import-submit-utils.spec.ts @@ -143,7 +143,7 @@ describe('Import Submit Utils', () => { mockData.resources = Resources.KnativeService; const imageStreamSpy = jest - .spyOn(submitUtils, 'createImageStream') + .spyOn(submitUtils, 'createOrUpdateImageStream') .mockImplementation(() => ({ status: { dockerImageReference: 'test:1234', diff --git a/frontend/packages/dev-console/src/components/import/advanced/AdvancedSection.tsx b/frontend/packages/dev-console/src/components/import/advanced/AdvancedSection.tsx index 3c5598247cc..a13705bfaa9 100644 --- a/frontend/packages/dev-console/src/components/import/advanced/AdvancedSection.tsx +++ b/frontend/packages/dev-console/src/components/import/advanced/AdvancedSection.tsx @@ -26,7 +26,7 @@ const AdvancedSection: React.FC = ({ values }) => { return ( - {values.route.show && } + => { const { project: { name: namespace }, @@ -62,7 +65,7 @@ export const createImageStream = ( labels: userLabels, } = formData; const defaultLabels = getAppLabels(name, application); - const imageStream = { + const newImageStream = { apiVersion: 'image.openshift.io/v1', kind: 'ImageStream', metadata: { @@ -88,7 +91,11 @@ export const createImageStream = ( }, }; - return k8sCreate(ImageStreamModel, imageStream, dryRun ? dryRunOpt : {}); + const imageStream = _.merge({}, originalImageStream || {}, newImageStream); + + return verb === 'update' + ? k8sUpdate(ImageStreamModel, imageStream) + : k8sCreate(ImageStreamModel, imageStream, dryRun ? dryRunOpt : {}); }; const getMetadata = (formData: DeployImageFormData) => { @@ -97,9 +104,10 @@ const getMetadata = (formData: DeployImageFormData) => { name, isi: { image }, labels: userLabels, + imageStream: { image: imgName, tag: imgTag, namespace: imgNamespace }, } = formData; - const defaultLabels = getAppLabels(name, application); + const defaultLabels = getAppLabels(name, application, imgName || undefined, imgTag, imgNamespace); const labels = { ...defaultLabels, ...userLabels }; const podLabels = getPodLabels(name); @@ -122,9 +130,11 @@ const getMetadata = (formData: DeployImageFormData) => { return { labels, podLabels, volumes, volumeMounts }; }; -export const createDeployment = ( +export const createOrUpdateDeployment = ( formData: DeployImageFormData, dryRun: boolean, + originalDeployment?: K8sResourceKind, + verb: K8sVerb = 'create', ): Promise => { const { registry, @@ -159,7 +169,7 @@ export const createDeployment = ( ? `${imgName || name}:${tag}` : _.get(image, 'dockerImageReference'); - const deployment = { + const newDeployment = { kind: 'Deployment', apiVersion: 'apps/v1', metadata: { @@ -209,12 +219,19 @@ export const createDeployment = ( }, }, }; - return k8sCreate(DeploymentModel, deployment, dryRun ? dryRunOpt : {}); + + const deployment = _.merge({}, originalDeployment || {}, newDeployment); + + return verb === 'update' + ? k8sUpdate(DeploymentModel, deployment) + : k8sCreate(DeploymentModel, deployment, dryRun ? dryRunOpt : {}); }; -export const createDeploymentConfig = ( +export const createOrUpdateDeploymentConfig = ( formData: DeployImageFormData, dryRun: boolean, + originalDeploymentConfig?: K8sResourceKind, + verb: K8sVerb = 'create', ): Promise => { const { project: { name: namespace }, @@ -228,7 +245,7 @@ export const createDeploymentConfig = ( const { labels, podLabels, volumes, volumeMounts } = getMetadata(formData); - const deploymentConfig = { + const newDeploymentConfig = { kind: 'DeploymentConfig', apiVersion: 'apps.openshift.io/v1', metadata: { @@ -290,7 +307,11 @@ export const createDeploymentConfig = ( }, }; - return k8sCreate(DeploymentConfigModel, deploymentConfig, dryRun ? dryRunOpt : {}); + const deploymentConfig = _.merge({}, originalDeploymentConfig || {}, newDeploymentConfig); + + return verb === 'update' + ? k8sUpdate(DeploymentConfigModel, deploymentConfig) + : k8sCreate(DeploymentConfigModel, deploymentConfig, dryRun ? dryRunOpt : {}); }; export const ensurePortExists = (formData: DeployImageFormData): DeployImageFormData => { @@ -317,15 +338,19 @@ export const ensurePortExists = (formData: DeployImageFormData): DeployImageForm return values; }; -export const createResources = async ( +export const createOrUpdateDeployImageResources = async ( rawFormData: DeployImageFormData, dryRun: boolean = false, + verb: K8sVerb = 'create', + appResources?: AppResources, ): Promise => { const formData = ensurePortExists(rawFormData); const { + name, registry, - route: { create: canCreateRoute }, - isi: { ports, tag: imageStreamTag }, + route: { create: canCreateRoute, disable }, + isi: { ports, tag: imageStreamTag, image }, + imageStream: { image: internalImageName, namespace: internalImageNamespace }, } = formData; const requests: Promise[] = []; @@ -334,28 +359,72 @@ export const createResources = async ( requests.push(createSystemImagePullerRoleBinding(formData, dryRun)); } if (formData.resources !== Resources.KnativeService) { - registry === RegistryType.External && requests.push(createImageStream(formData, dryRun)); + registry === RegistryType.External && + requests.push( + createOrUpdateImageStream(formData, dryRun, _.get(appResources, 'imageStream.data'), verb), + ); if (formData.resources === Resources.Kubernetes) { - requests.push(createDeployment(formData, dryRun)); + requests.push( + createOrUpdateDeployment( + formData, + dryRun, + _.get(appResources, 'editAppResource.data'), + verb, + ), + ); } else { - requests.push(createDeploymentConfig(formData, dryRun)); + requests.push( + createOrUpdateDeploymentConfig( + formData, + dryRun, + _.get(appResources, 'editAppResource.data'), + verb, + ), + ); } if (!_.isEmpty(ports)) { - const service = createService(formData); - requests.push(k8sCreate(ServiceModel, service, dryRun ? dryRunOpt : {})); - if (canCreateRoute) { - const route = createRoute(formData); + const service = createService(formData, undefined, _.get(appResources, 'service.data')); + requests.push( + verb === 'update' + ? k8sUpdate(ServiceModel, service) + : k8sCreate(ServiceModel, service, dryRun ? dryRunOpt : {}), + ); + const route = createRoute(formData, undefined, _.get(appResources, 'route.data')); + if (verb === 'update' && disable) { + requests.push(k8sUpdate(RouteModel, route)); + } else if (canCreateRoute) { requests.push(k8sCreate(RouteModel, route, dryRun ? dryRunOpt : {})); } } } else if (!dryRun) { // Do not run serverless call during the dry run. - const imageStreamResponse = await createImageStream(formData, dryRun); + let imageStreamRepo: string = _.split(_.get(image, 'dockerImageReference', ''), '@')[0]; + if (registry === RegistryType.External) { + const imageStreamResponse = await createOrUpdateImageStream( + formData, + dryRun, + _.get(appResources, 'imageStream.data'), + verb, + ); + imageStreamRepo = imageStreamResponse.status.dockerImageRepository; + } const imageStreamUrl = imageStreamTag - ? `${imageStreamResponse.status.dockerImageRepository}:${imageStreamTag}` - : imageStreamResponse.status.dockerImageRepository; - const knDeploymentResource = getKnativeServiceDepResource(formData, imageStreamUrl); - requests.push(k8sCreate(KnServiceModel, knDeploymentResource)); + ? `${imageStreamRepo}:${imageStreamTag}` + : imageStreamRepo; + const knDeploymentResource = getKnativeServiceDepResource( + formData, + imageStreamUrl, + internalImageName || name, + imageStreamTag, + internalImageNamespace, + undefined, + _.get(appResources, 'editAppResource.data'), + ); + requests.push( + verb === 'update' + ? k8sUpdate(KnServiceModel, knDeploymentResource) + : k8sCreate(KnServiceModel, knDeploymentResource), + ); } return Promise.all(requests); diff --git a/frontend/packages/dev-console/src/components/import/image-search/ImageSearch.tsx b/frontend/packages/dev-console/src/components/import/image-search/ImageSearch.tsx index 0608efb5ef6..d6dc362189d 100644 --- a/frontend/packages/dev-console/src/components/import/image-search/ImageSearch.tsx +++ b/frontend/packages/dev-console/src/components/import/image-search/ImageSearch.tsx @@ -10,74 +10,82 @@ import { getSuggestedName, getPorts, makePortName } from '../../../utils/imagest import { secretModalLauncher } from '../CreateSecretModal'; const ImageSearch: React.FC = () => { - const { values, setFieldValue, setFieldError } = useFormikContext(); + const { values, setFieldValue, setFieldError, dirty } = useFormikContext(); const [newImageSecret, setNewImageSecret] = React.useState(''); const [alertVisible, shouldHideAlert] = React.useState(true); const namespace = values.project.name; - const handleSearch = (searchTerm: string) => { - const importImage = { - kind: 'ImageStreamImport', - apiVersion: 'image.openshift.io/v1', - metadata: { - name: 'newapp', - namespace: values.project.name, - }, - spec: { - import: false, - images: [ - { - from: { - kind: 'DockerImage', - name: _.trim(searchTerm), + + const handleSearch = React.useCallback( + (searchTerm: string) => { + const importImage = { + kind: 'ImageStreamImport', + apiVersion: 'image.openshift.io/v1', + metadata: { + name: 'newapp', + namespace: values.project.name, + }, + spec: { + import: false, + images: [ + { + from: { + kind: 'DockerImage', + name: _.trim(searchTerm), + }, }, - }, - ], - }, - status: {}, - }; + ], + }, + status: {}, + }; - k8sCreate(ImageStreamImportsModel, importImage) - .then((imageStreamImport) => { - const status = _.get(imageStreamImport, 'status.images[0].status'); - if (status.status === 'Success') { - const name = _.get(imageStreamImport, 'spec.images[0].from.name'); - const image = _.get(imageStreamImport, 'status.images[0].image'); - const tag = _.get(imageStreamImport, 'status.images[0].tag'); - const isi = { name, image, tag, status }; - const ports = getPorts(isi); - setFieldValue('isSearchingForImage', false); - setFieldValue('isi.name', name); - setFieldValue('isi.image', image); - setFieldValue('isi.tag', tag); - setFieldValue('isi.status', status); - setFieldValue('isi.ports', ports); - setFieldValue('image.ports', ports); - setFieldValue('image.tag', tag); - !values.name && setFieldValue('name', getSuggestedName(name)); - !values.application.name && - setFieldValue('application.name', `${getSuggestedName(name)}-app`); - // set default port value - const targetPort = _.head(ports); - targetPort && setFieldValue('route.targetPort', makePortName(targetPort)); - } else { - setFieldValue('isSearchingForImage', false); + k8sCreate(ImageStreamImportsModel, importImage) + .then((imageStreamImport) => { + const status = _.get(imageStreamImport, 'status.images[0].status'); + if (status.status === 'Success') { + const name = _.get(imageStreamImport, 'spec.images[0].from.name'); + const image = _.get(imageStreamImport, 'status.images[0].image'); + const tag = _.get(imageStreamImport, 'status.images[0].tag'); + const isi = { name, image, tag, status }; + const ports = getPorts(isi); + setFieldValue('isSearchingForImage', false); + setFieldValue('isi.name', name); + setFieldValue('isi.image', image); + setFieldValue('isi.tag', tag); + setFieldValue('isi.status', status); + setFieldValue('isi.ports', ports); + setFieldValue('image.ports', ports); + setFieldValue('image.tag', tag); + !values.name && setFieldValue('name', getSuggestedName(name)); + !values.application.name && + setFieldValue('application.name', `${getSuggestedName(name)}-app`); + // set default port value + const targetPort = _.head(ports); + targetPort && setFieldValue('route.targetPort', makePortName(targetPort)); + } else { + setFieldValue('isSearchingForImage', false); + setFieldValue('isi', {}); + setFieldError('isi.image', status.message); + setFieldValue('route.targetPort', null); + } + }) + .catch((error) => { + setFieldError('isi.image', error.message); setFieldValue('isi', {}); - setFieldError('isi.image', status.message); - setFieldValue('route.targetPort', null); - } - }) - .catch((error) => { - setFieldError('isi.image', error.message); - setFieldValue('isi', {}); - setFieldValue('isSearchingForImage', false); - }); - }; + setFieldValue('isSearchingForImage', false); + }); + }, + [setFieldError, setFieldValue, values.application.name, values.name, values.project.name], + ); const handleSave = (name: string) => { setNewImageSecret(name); values.searchTerm && handleSearch(values.searchTerm); }; + React.useEffect(() => { + !dirty && values.searchTerm && handleSearch(values.searchTerm); + }, [dirty, handleSearch, values.searchTerm]); + return ( <> { { label: imageRegistryType.External.label, value: imageRegistryType.External.value, + isDisabled: values.formType === 'edit' && values.registry === 'internal', activeChildren: , }, { label: imageRegistryType.Internal.label, value: imageRegistryType.Internal.value, + isDisabled: values.formType === 'edit' && values.registry === 'external', activeChildren: , }, ]} diff --git a/frontend/packages/dev-console/src/components/import/image-search/ImageStreamDropdown.tsx b/frontend/packages/dev-console/src/components/import/image-search/ImageStreamDropdown.tsx index f2118babb3b..cb7de89c278 100644 --- a/frontend/packages/dev-console/src/components/import/image-search/ImageStreamDropdown.tsx +++ b/frontend/packages/dev-console/src/components/import/image-search/ImageStreamDropdown.tsx @@ -34,12 +34,23 @@ const ImageStreamDropdown: React.FC = () => { ? 'No Image Stream' : 'Select Image Stream'; }; - const onDropdownChange = (img: string) => { - setFieldValue('imageStream.tag', ''); - setFieldValue('isi', initialValues.isi); - const image = imgCollection[imageStream.namespace][img]; - dispatch({ type: ImageStreamActions.setSelectedImageStream, value: image }); - }; + + const onDropdownChange = React.useCallback( + (img: string) => { + setFieldValue('imageStream.tag', initialValues.imageStream.tag); + setFieldValue('isi', initialValues.isi); + const image = _.get(imgCollection, [imageStream.namespace, img], {}); + dispatch({ type: ImageStreamActions.setSelectedImageStream, value: image }); + }, + [ + setFieldValue, + initialValues.imageStream.tag, + initialValues.isi, + imgCollection, + imageStream.namespace, + dispatch, + ], + ); const onLoad = (imgstreams) => { const imageStreamAvailable = !_.isEmpty(imgstreams); setHasImageStreams(imageStreamAvailable); @@ -54,6 +65,18 @@ const ImageStreamDropdown: React.FC = () => { collectImageStreams(namespace, resource); return namespace === imageStream.namespace; }; + + React.useEffect(() => { + imageStream.image && onDropdownChange(imageStream.image); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [imageStream.image, isStreamsAvailable]); + + React.useEffect(() => { + if (initialValues.imageStream.image !== imageStream.image) { + initialValues.imageStream.tag = ''; + } + }, [imageStream.image, initialValues.imageStream.image, initialValues.imageStream.tag]); + return ( { key={imageStream.namespace} fullWidth required - title={getTitle()} + title={imageStream.image || getTitle()} disabled={!hasCreateAccess || !isStreamsAvailable} onChange={onDropdownChange} onLoad={onLoad} diff --git a/frontend/packages/dev-console/src/components/import/image-search/ImageStreamNsDropdown.tsx b/frontend/packages/dev-console/src/components/import/image-search/ImageStreamNsDropdown.tsx index 7cb79f6b954..46f7a661661 100644 --- a/frontend/packages/dev-console/src/components/import/image-search/ImageStreamNsDropdown.tsx +++ b/frontend/packages/dev-console/src/components/import/image-search/ImageStreamNsDropdown.tsx @@ -9,41 +9,67 @@ import { ImageStreamActions as Action } from '../import-types'; import { ImageStreamContext } from './ImageStreamContext'; const ImageStreamNsDropdown: React.FC = () => { - const { setFieldValue, initialValues } = useFormikContext(); + const { values, setFieldValue, initialValues } = useFormikContext(); const { dispatch } = React.useContext(ImageStreamContext); - const onDropdownChange = (selectedProject: string) => { - const promiseArr = []; - setFieldValue('imageStream.image', ''); - setFieldValue('imageStream.tag', ''); - setFieldValue('isi', initialValues.isi); - dispatch({ type: Action.setLoading, value: true }); - dispatch({ type: Action.setAccessLoading, value: true }); - promiseArr.push( - checkAccess({ - group: RoleBindingModel.apiGroup, - resource: RoleBindingModel.plural, - verb: 'create', - name: 'system:image-puller', - namespace: selectedProject, - }) - .then((resp) => dispatch({ type: Action.setHasCreateAccess, value: resp.status.allowed })) - .catch(() => dispatch({ type: Action.setHasAccessToPullImage, value: false })), - ); - promiseArr.push( - k8sGet(RoleBindingModel, 'system:image-puller', selectedProject) - .then(() => { - dispatch({ - type: Action.setHasAccessToPullImage, - value: true, - }); - setFieldValue('imageStream.grantAccess', false); + const onDropdownChange = React.useCallback( + (selectedProject: string) => { + const promiseArr = []; + setFieldValue('imageStream.image', initialValues.imageStream.image); + setFieldValue('imageStream.tag', initialValues.imageStream.tag); + setFieldValue('isi', initialValues.isi); + dispatch({ type: Action.setLoading, value: true }); + dispatch({ type: Action.setAccessLoading, value: true }); + promiseArr.push( + checkAccess({ + group: RoleBindingModel.apiGroup, + resource: RoleBindingModel.plural, + verb: 'create', + name: 'system:image-puller', + namespace: selectedProject, }) - .catch(() => dispatch({ type: Action.setHasAccessToPullImage, value: false })), - ); - return Promise.all(promiseArr).then(() => - dispatch({ type: Action.setAccessLoading, value: false }), - ); - }; + .then((resp) => dispatch({ type: Action.setHasCreateAccess, value: resp.status.allowed })) + .catch(() => dispatch({ type: Action.setHasAccessToPullImage, value: false })), + ); + promiseArr.push( + k8sGet(RoleBindingModel, 'system:image-puller', selectedProject) + .then(() => { + dispatch({ + type: Action.setHasAccessToPullImage, + value: true, + }); + setFieldValue('imageStream.grantAccess', false); + }) + .catch(() => dispatch({ type: Action.setHasAccessToPullImage, value: false })), + ); + return Promise.all(promiseArr).then(() => + dispatch({ type: Action.setAccessLoading, value: false }), + ); + }, + [ + dispatch, + initialValues.imageStream.image, + initialValues.imageStream.tag, + initialValues.isi, + setFieldValue, + ], + ); + + React.useEffect(() => { + values.imageStream.namespace && onDropdownChange(values.imageStream.namespace); + }, [onDropdownChange, values.imageStream.namespace]); + + React.useEffect(() => { + if (initialValues.imageStream.namespace !== values.imageStream.namespace) { + initialValues.imageStream.image = ''; + initialValues.imageStream.tag = ''; + } + }, [ + initialValues.imageStream.image, + initialValues.imageStream.namespace, + initialValues.imageStream.tag, + values.imageStream.namespace, + ]); + return ( { let imageStreamTagList = {}; const { - values: { imageStream, application }, + values: { imageStream, application, formType }, setFieldValue, setFieldError, } = useFormikContext(); @@ -42,7 +42,7 @@ const ImageStreamTagDropdown: React.FC = () => { setFieldValue('isi.tag', selectedTag); setFieldValue('isi.ports', ports); setFieldValue('image.ports', ports); - setFieldValue('name', getSuggestedName(name)); + formType !== 'edit' && setFieldValue('name', getSuggestedName(name)); !application.name && setFieldValue('application.name', `${getSuggestedName(name)}-app`); // set default port value const targetPort = _.head(ports); @@ -54,8 +54,20 @@ const ImageStreamTagDropdown: React.FC = () => { setFieldValue('isSearchingForImage', false); }); }, - [setFieldValue, imageStream, application.name, setFieldError], + [ + setFieldValue, + imageStream.image, + imageStream.namespace, + formType, + application.name, + setFieldError, + ], ); + + React.useEffect(() => { + imageStream.tag && searchImageTag(imageStream.tag); + }, [imageStream.tag, searchImageTag]); + return ( { items={imageStreamTagList} key={imageStream.image} title={ - isNamespaceSelected && isImageStreamSelected && !isTagsAvailable ? 'No Tag' : 'Select Tag' + imageStream.tag || + (isNamespaceSelected && isImageStreamSelected && !isTagsAvailable ? 'No Tag' : 'Select Tag') } disabled={!isImageStreamSelected || !isTagsAvailable} fullWidth diff --git a/frontend/packages/dev-console/src/components/import/image-search/SearchResults.tsx b/frontend/packages/dev-console/src/components/import/image-search/SearchResults.tsx index 6703744ae37..d8d3326c395 100644 --- a/frontend/packages/dev-console/src/components/import/image-search/SearchResults.tsx +++ b/frontend/packages/dev-console/src/components/import/image-search/SearchResults.tsx @@ -47,7 +47,9 @@ const SearchResults: React.FC = () => { {_.get(values.isi, 'result.ref.registry') && ( from {values.isi.result.ref.registry}, )} - ,{' '} + {_.get(values.isi, 'image.dockerImageMetadata.Created') && ( + + )} {_.get(values.isi, 'image.dockerImageMetadata.Size') && ( { @@ -80,7 +82,7 @@ const SearchResults: React.FC = () => { )} - {!_.isEmpty(values.isi.image.dockerImageMetadata.Config.Volumes) && ( + {!_.isEmpty(_.get(values.isi, 'image.dockerImageMetadata.Config.Volumes')) && (

This image declares volumes and will default to use non-persistent, host-local storage. You can add persistent storage later to the deployment config. diff --git a/frontend/packages/dev-console/src/components/import/import-submit-utils.ts b/frontend/packages/dev-console/src/components/import/import-submit-utils.ts index 0b244eca65d..0adb9209ef4 100644 --- a/frontend/packages/dev-console/src/components/import/import-submit-utils.ts +++ b/frontend/packages/dev-console/src/components/import/import-submit-utils.ts @@ -15,6 +15,8 @@ import { ServiceModel as KnServiceModel, } from '@console/knative-plugin'; import { SecretType } from '@console/internal/components/secrets/create-secret'; +import * as plugins from '@console/internal/plugins'; +import { history } from '@console/internal/components/utils'; import { getAppLabels, getPodLabels, getAppAnnotations } from '../../utils/resource-label-utils'; import { createService, createRoute, dryRunOpt } from '../../utils/shared-submit-utils'; import { AppResources } from '../edit-application/edit-application-types'; @@ -48,10 +50,12 @@ export const createProject = (projectData: ProjectData): Promise => { const { name, @@ -64,7 +68,7 @@ export const createImageStream = ( const imageStreamName = imageStreamData && imageStreamData.metadata.name; const defaultLabels = getAppLabels(name, application, imageStreamName, tag); const defaultAnnotations = getAppAnnotations(repository, ref); - const imageStream = { + const newImageStream = { apiVersion: 'image.openshift.io/v1', kind: 'ImageStream', metadata: { @@ -75,7 +79,11 @@ export const createImageStream = ( }, }; - return k8sCreate(ImageStreamModel, imageStream, dryRun ? dryRunOpt : {}); + const imageStream = _.merge({}, originalImageStream || {}, newImageStream); + + return verb === 'update' + ? k8sUpdate(ImageStreamModel, imageStream) + : k8sCreate(ImageStreamModel, imageStream, dryRun ? dryRunOpt : {}); }; export const createWebhookSecret = ( @@ -155,19 +163,16 @@ export const createOrUpdateBuildConfig = ( }, }; - const buildConfig = { - ...(originalBuildConfig || {}), + const newBuildConfig = { apiVersion: 'build.openshift.io/v1', kind: 'BuildConfig', metadata: { - ...(originalBuildConfig ? originalBuildConfig.metadata : {}), name, namespace, labels: { ...defaultLabels, ...userLabels }, annotations: defaultAnnotations, }, spec: { - ...(originalBuildConfig ? originalBuildConfig.spec : {}), output: { to: { kind: 'ImageStreamTag', @@ -201,6 +206,8 @@ export const createOrUpdateBuildConfig = ( }, }; + const buildConfig = _.merge({}, originalBuildConfig || {}, newBuildConfig); + return verb === 'update' ? k8sUpdate(BuildConfigModel, buildConfig) : k8sCreate(BuildConfigModel, buildConfig, dryRun ? dryRunOpt : {}); @@ -238,19 +245,16 @@ export const createOrUpdateDeployment = ( }; const podLabels = getPodLabels(name); - const deployment = { - ...(originalDeployment || {}), + const newDeployment = { apiVersion: 'apps/v1', kind: 'Deployment', metadata: { - ...(originalDeployment ? originalDeployment.metadata : {}), name, namespace, labels: { ...defaultLabels, ...userLabels }, annotations, }, spec: { - ...(originalDeployment ? originalDeployment.spec : {}), selector: { matchLabels: { app: name, @@ -289,6 +293,8 @@ export const createOrUpdateDeployment = ( }, }; + const deployment = _.merge({}, originalDeployment || {}, newDeployment); + return verb === 'update' ? k8sUpdate(DeploymentModel, deployment) : k8sCreate(DeploymentModel, deployment, dryRun ? dryRunOpt : {}); @@ -317,19 +323,16 @@ export const createOrUpdateDeploymentConfig = ( const defaultAnnotations = getAppAnnotations(repository, ref); const podLabels = getPodLabels(name); - const deploymentConfig = { - ...(originalDeploymentConfig || {}), + const newDeploymentConfig = { apiVersion: 'apps.openshift.io/v1', kind: 'DeploymentConfig', metadata: { - ...(originalDeploymentConfig ? originalDeploymentConfig.metadata : {}), name, namespace, labels: { ...defaultLabels, ...userLabels }, annotations: defaultAnnotations, }, spec: { - ...(originalDeploymentConfig ? originalDeploymentConfig.spec : {}), selector: podLabels, replicas, template: { @@ -378,6 +381,8 @@ export const createOrUpdateDeploymentConfig = ( }, }; + const deploymentConfig = _.merge({}, originalDeploymentConfig || {}, newDeploymentConfig); + return verb === 'update' ? k8sUpdate(DeploymentConfigModel, deploymentConfig) : k8sCreate(DeploymentConfigModel, deploymentConfig, dryRun ? dryRunOpt : {}); @@ -390,12 +395,11 @@ export const createOrUpdateResources = async ( dryRun: boolean = false, verb: K8sVerb = 'create', appResources?: AppResources, - editAppResource?: K8sResourceKind, ): Promise => { const { name, project: { name: namespace }, - route: { create: canCreateRoute, show: showRouteCheckbox }, + route: { create: canCreateRoute, disable }, image: { ports }, build: { strategy: buildStrategy, @@ -410,22 +414,25 @@ export const createOrUpdateResources = async ( const requests: Promise[] = []; - verb === 'create' && - requests.push( - createImageStream(formData, imageStream, dryRun), - createWebhookSecret(formData, 'generic', dryRun), - ); - requests.push( + createOrUpdateImageStream( + formData, + imageStream, + dryRun, + _.get(appResources, 'imageStream.data'), + verb, + ), createOrUpdateBuildConfig( formData, imageStream, dryRun, - _.get(appResources, 'buildConfig.data', null), + _.get(appResources, 'buildConfig.data'), verb, ), ); + verb === 'create' && requests.push(createWebhookSecret(formData, 'generic', dryRun)); + const defaultAnnotations = getAppAnnotations(repository, ref); if (formData.resources === Resources.KnativeService) { @@ -433,49 +440,57 @@ export const createOrUpdateResources = async ( if (dryRun) { return Promise.all(requests); } - let imageStreamURL: string; - const knativeRequests = []; - if (verb === 'update') { - imageStreamURL = _.get(editAppResource, 'spec.template.spec.containers[0].image', ''); - knativeRequests.push(...requests); - } else { - const [imageStreamResponse] = await Promise.all(requests); - imageStreamURL = imageStreamResponse.status.dockerImageRepository; - } + const [imageStreamResponse] = await Promise.all(requests); + const imageStreamURL = imageStreamResponse.status.dockerImageRepository; const knDeploymentResource = getKnativeServiceDepResource( formData, imageStreamURL, imageStreamName, + undefined, + undefined, defaultAnnotations, - editAppResource, + _.get(appResources, 'editAppResource.data'), ); - knativeRequests.push( + return Promise.all([ verb === 'update' ? k8sUpdate(KnServiceModel, knDeploymentResource) : k8sCreate(KnServiceModel, knDeploymentResource), - ); - return Promise.all(knativeRequests); + ]); } if (formData.resources === Resources.Kubernetes) { - requests.push(createOrUpdateDeployment(formData, imageStream, dryRun, editAppResource, verb)); + requests.push( + createOrUpdateDeployment( + formData, + imageStream, + dryRun, + _.get(appResources, 'editAppResource.data'), + verb, + ), + ); } else if (formData.resources === Resources.OpenShift) { requests.push( - createOrUpdateDeploymentConfig(formData, imageStream, dryRun, editAppResource, verb), + createOrUpdateDeploymentConfig( + formData, + imageStream, + dryRun, + _.get(appResources, 'editAppResource.data'), + verb, + ), ); } if (!_.isEmpty(ports) || buildStrategy === 'Docker') { - const originalService = _.get(appResources, 'service.data', null); + const originalService = _.get(appResources, 'service.data'); const service = createService(formData, imageStream, originalService); requests.push( verb === 'update' ? k8sUpdate(ServiceModel, service) : k8sCreate(ServiceModel, service, dryRun ? dryRunOpt : {}), ); - const originalRoute = _.get(appResources, 'route.data', null); + const originalRoute = _.get(appResources, 'route.data'); const route = createRoute(formData, imageStream, originalRoute); - if (verb === 'update' && !showRouteCheckbox) { + if (verb === 'update' && disable) { requests.push(k8sUpdate(RouteModel, route, namespace, name)); } else if (canCreateRoute) { requests.push(k8sCreate(RouteModel, route, dryRun ? dryRunOpt : {})); @@ -492,3 +507,11 @@ export const createOrUpdateResources = async ( return Promise.all(requests); }; + +export const handleRedirect = (project: string, perspective: string) => { + const perspectiveData = plugins.registry + .getPerspectives() + .find((item) => item.properties.id === perspective); + const redirectURL = perspectiveData.properties.getImportRedirectURL(project); + history.push(redirectURL); +}; diff --git a/frontend/packages/dev-console/src/components/import/import-types.ts b/frontend/packages/dev-console/src/components/import/import-types.ts index 9a9fcd6beb3..8f85375f729 100644 --- a/frontend/packages/dev-console/src/components/import/import-types.ts +++ b/frontend/packages/dev-console/src/components/import/import-types.ts @@ -52,6 +52,7 @@ export interface FirehoseList { } export interface DeployImageFormData { + formType?: string; project: ProjectData; application: ApplicationData; name: string; @@ -70,7 +71,7 @@ export interface DeployImageFormData { serverless?: ServerlessData; pipeline?: PipelineData; labels: { [name: string]: string }; - env: { [name: string]: string }; + env?: { [name: string]: string }; route: RouteData; build: BuildData; deployment: DeploymentData; @@ -96,7 +97,7 @@ export interface GitImportFormData { } export interface ApplicationData { - initial: string; + initial?: string; name: string; selectedKey: string; } @@ -121,8 +122,8 @@ export interface ImageStreamImageData { export interface ProjectData { name: string; - displayName: string; - description: string; + displayName?: string; + description?: string; } export interface GitData { @@ -142,7 +143,7 @@ export interface DockerData { } export interface RouteData { - show?: boolean; + disable?: boolean; create: boolean; targetPort: string; unknownTargetPort?: string; @@ -164,9 +165,9 @@ export interface TLSData { export interface BuildData { triggers: { - webhook: boolean; - image: boolean; - config: boolean; + webhook?: boolean; + image?: boolean; + config?: boolean; }; env: (NameValuePair | NameValueFromPair)[]; strategy: string; diff --git a/frontend/packages/dev-console/src/components/import/route/RouteCheckbox.tsx b/frontend/packages/dev-console/src/components/import/route/RouteCheckbox.tsx index 32346636dd1..c6d0d4a9088 100644 --- a/frontend/packages/dev-console/src/components/import/route/RouteCheckbox.tsx +++ b/frontend/packages/dev-console/src/components/import/route/RouteCheckbox.tsx @@ -1,12 +1,17 @@ import * as React from 'react'; import { CheckboxField } from '@console/shared'; -const RouteCheckbox: React.FC = () => { +interface RouteCheckboxProps { + isDisabled?: boolean; +} + +const RouteCheckbox: React.FC = ({ isDisabled }) => { return ( ); }; diff --git a/frontend/packages/dev-console/src/components/modals/index.ts b/frontend/packages/dev-console/src/components/modals/index.ts index e6fe7b36228..b4affe7cf89 100644 --- a/frontend/packages/dev-console/src/components/modals/index.ts +++ b/frontend/packages/dev-console/src/components/modals/index.ts @@ -7,8 +7,3 @@ export const deleteApplicationModal = (props) => import('./DeleteApplicationModal' /* webpackChunkName: "delete-application-modal" */).then((m) => m.deleteApplicationModal(props), ); - -export const editApplication = (props) => - import( - '../edit-application/EditApplicationWrapper' /* webpackChunkName: "edit-application-modal" */ - ).then((m) => m.editApplication(props)); diff --git a/frontend/packages/dev-console/src/plugin.tsx b/frontend/packages/dev-console/src/plugin.tsx index caea61b3fd7..c8ca27f2ac9 100644 --- a/frontend/packages/dev-console/src/plugin.tsx +++ b/frontend/packages/dev-console/src/plugin.tsx @@ -377,6 +377,7 @@ const plugin: Plugin = [ '/metrics', '/project-access', '/dev-monitoring', + '/edit', ], component: NamespaceRedirect, }, @@ -408,6 +409,19 @@ const plugin: Plugin = [ ).default, }, }, + { + type: 'Page/Route', + properties: { + exact: true, + path: '/edit/ns/:ns', + loader: async () => + ( + await import( + './components/edit-application/EditApplicationPage' /* webpackChunkName: "dev-console-edit" */ + ) + ).default, + }, + }, { type: 'Page/Route', properties: { diff --git a/frontend/packages/dev-console/src/utils/resource-label-utils.ts b/frontend/packages/dev-console/src/utils/resource-label-utils.ts index 06f2138bfdd..cafbec16474 100644 --- a/frontend/packages/dev-console/src/utils/resource-label-utils.ts +++ b/frontend/packages/dev-console/src/utils/resource-label-utils.ts @@ -3,6 +3,7 @@ export const getAppLabels = ( application?: string, imageStreamName?: string, selectedTag?: string, + namespace?: string, ) => { const labels = { app: name, @@ -18,6 +19,9 @@ export const getAppLabels = ( if (selectedTag) { labels['app.openshift.io/runtime-version'] = selectedTag; } + if (namespace) { + labels['app.openshift.io/runtime-namespace'] = namespace; + } return labels; }; diff --git a/frontend/packages/dev-console/src/utils/shared-submit-utils.ts b/frontend/packages/dev-console/src/utils/shared-submit-utils.ts index 689476b9f31..de24d60d4a6 100644 --- a/frontend/packages/dev-console/src/utils/shared-submit-utils.ts +++ b/frontend/packages/dev-console/src/utils/shared-submit-utils.ts @@ -47,19 +47,16 @@ export const createService = ( ports = isiPorts; } - const service: any = { - ...(originalService || {}), + const newService: any = { kind: 'Service', apiVersion: 'v1', metadata: { - ...(originalService ? originalService.metadata : {}), name, namespace, labels: { ...defaultLabels, ...userLabels }, annotations: { ...defaultAnnotations }, }, spec: { - ...(originalService ? originalService.spec : {}), selector: podLabels, ports: _.map(ports, (port) => ({ port: port.containerPort, @@ -71,6 +68,8 @@ export const createService = ( }, }; + const service = _.merge({}, originalService || {}, newService); + return service; }; @@ -113,19 +112,16 @@ export const createRoute = ( targetPort = routeTargetPort || makePortName(_.head(ports)); } - const route: any = { - ...(originalRoute || {}), + const newRoute: any = { kind: 'Route', apiVersion: 'route.openshift.io/v1', metadata: { - ...(originalRoute ? originalRoute.metadata : {}), name, namespace, labels: { ...defaultLabels, ...userLabels }, defaultAnnotations, }, spec: { - ...(originalRoute ? originalRoute.spec : {}), to: { kind: 'Service', name, @@ -144,5 +140,8 @@ export const createRoute = ( wildcardPolicy: 'None', }, }; + + const route = _.merge({}, originalRoute || {}, newRoute); + return route; }; diff --git a/frontend/packages/knative-plugin/src/utils/create-knative-utils.ts b/frontend/packages/knative-plugin/src/utils/create-knative-utils.ts index 8e713a5abbb..c1bcec6ecde 100644 --- a/frontend/packages/knative-plugin/src/utils/create-knative-utils.ts +++ b/frontend/packages/knative-plugin/src/utils/create-knative-utils.ts @@ -16,11 +16,14 @@ import { DeployImageFormData, GitImportFormData, } from '@console/dev-console/src/components/import/import-types'; +import * as _ from 'lodash'; export const getKnativeServiceDepResource = ( formData: GitImportFormData | DeployImageFormData, imageStreamUrl: string, imageStreamName?: string, + imageStreamTag?: string, + imageNamespace?: string, annotations?: { [name: string]: string }, originalKnativeService?: K8sResourceKind, ): K8sResourceKind => { @@ -50,14 +53,18 @@ export const getKnativeServiceDepResource = ( limitUnit: memoryLimitUnit, }, } = limits; - const defaultLabel = getAppLabels(name, applicationName, imageStreamName, imageTag); + const defaultLabel = getAppLabels( + name, + applicationName, + imageStreamName, + imageStreamTag || imageTag, + imageNamespace, + ); delete defaultLabel.app; - const knativeDeployResource: K8sResourceKind = { - ...(originalKnativeService || {}), + const newKnativeDeployResource: K8sResourceKind = { kind: ServiceModel.kind, apiVersion: `${ServiceModel.apiGroup}/${ServiceModel.apiVersion}`, metadata: { - ...(originalKnativeService ? originalKnativeService.metadata : {}), name, namespace, labels: { @@ -67,7 +74,6 @@ export const getKnativeServiceDepResource = ( }, }, spec: { - ...(originalKnativeService ? originalKnativeService.spec : {}), template: { metadata: { labels: { @@ -115,6 +121,9 @@ export const getKnativeServiceDepResource = ( }, }, }; + + const knativeDeployResource = _.merge({}, originalKnativeService || {}, newKnativeDeployResource); + return knativeDeployResource; };