From c9148ae6da4f4ec148ef46b207ec55aef2d3f251 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 18 May 2022 13:34:07 +0200 Subject: [PATCH 1/5] Add integration test --- .../integration/devfile/cmd_devfile_deploy_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/devfile/cmd_devfile_deploy_test.go b/tests/integration/devfile/cmd_devfile_deploy_test.go index 8a960dea13c..6be08c9f3fd 100644 --- a/tests/integration/devfile/cmd_devfile_deploy_test.go +++ b/tests/integration/devfile/cmd_devfile_deploy_test.go @@ -129,6 +129,20 @@ var _ = Describe("odo devfile deploy command tests", func() { }) }) + + When("running and stopping odo dev", func() { + BeforeEach(func() { + session, _, _, _, err := helper.StartDevMode() + Expect(err).ShouldNot(HaveOccurred()) + session.Stop() + session.WaitEnd() + }) + + It("should not delete the resources created with odo deploy", func() { + output := commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Out.Contents() + Expect(string(output)).To(ContainSubstring(deploymentName)) + }) + }) }) }) } From 0081378bf00de95cb8ce6ce9ca61c05f479e7536 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 18 May 2022 13:54:04 +0200 Subject: [PATCH 2/5] Fix issue --- pkg/component/delete/delete.go | 84 ++++++++++--------- pkg/component/delete/delete_test.go | 3 +- pkg/component/delete/interface.go | 3 +- pkg/component/delete/mock.go | 8 +- pkg/odo/cli/delete/component/component.go | 3 +- .../cli/delete/component/component_test.go | 11 +-- pkg/odo/cli/dev/dev.go | 4 +- pkg/watch/interface.go | 4 +- pkg/watch/mock.go | 12 +-- pkg/watch/watch.go | 7 +- 10 files changed, 75 insertions(+), 64 deletions(-) diff --git a/pkg/component/delete/delete.go b/pkg/component/delete/delete.go index 82644fe1006..6e5b0a1d72a 100644 --- a/pkg/component/delete/delete.go +++ b/pkg/component/delete/delete.go @@ -87,54 +87,60 @@ func references(list []unstructured.Unstructured, ownerRef metav1.OwnerReference } // ListResourcesToDeleteFromDevfile parses all the devfile components and returns a list of resources that are present on the cluster and can be deleted -func (do DeleteComponentClient) ListResourcesToDeleteFromDevfile(devfileObj parser.DevfileObj, appName string) (isInnerLoopDeployed bool, resources []unstructured.Unstructured, err error) { - // Inner Loop - // Fetch the deployment of the devfile component - componentName := devfileObj.GetMetadataName() - var deploymentName string - deploymentName, err = util.NamespaceKubernetesObject(componentName, appName) - if err != nil { - return isInnerLoopDeployed, resources, fmt.Errorf("failed to get the resource %q name for component %q; cause: %w", kclient.DeploymentKind, deploymentName, err) - } +func (do DeleteComponentClient) ListResourcesToDeleteFromDevfile(devfileObj parser.DevfileObj, appName string, mode string) (isInnerLoopDeployed bool, resources []unstructured.Unstructured, err error) { + + if mode == odolabels.ComponentDevMode || mode == odolabels.ComponentAnyMode { + // Inner Loop + // Fetch the deployment of the devfile component + componentName := devfileObj.GetMetadataName() + var deploymentName string + deploymentName, err = util.NamespaceKubernetesObject(componentName, appName) + if err != nil { + return isInnerLoopDeployed, resources, fmt.Errorf("failed to get the resource %q name for component %q; cause: %w", kclient.DeploymentKind, deploymentName, err) + } - deployment, err := do.kubeClient.GetDeploymentByName(deploymentName) - if err != nil && !kerrors.IsNotFound(err) { - return isInnerLoopDeployed, resources, err - } + deployment, err := do.kubeClient.GetDeploymentByName(deploymentName) + if err != nil && !kerrors.IsNotFound(err) { + return isInnerLoopDeployed, resources, err + } - // if the deployment is found on the cluster, - // then convert it to unstructured.Unstructured object so that it can be appended to resources; - // else continue to outer loop - if deployment.Name != "" { - isInnerLoopDeployed = true - var unstructuredDeploy unstructured.Unstructured - unstructuredDeploy, err = kclient.ConvertK8sResourceToUnstructured(deployment) - if err != nil { - return isInnerLoopDeployed, resources, fmt.Errorf("failed to parse the resource %q: %q; cause: %w", kclient.DeploymentKind, deploymentName, err) + // if the deployment is found on the cluster, + // then convert it to unstructured.Unstructured object so that it can be appended to resources; + // else continue to outer loop + if deployment.Name != "" { + isInnerLoopDeployed = true + var unstructuredDeploy unstructured.Unstructured + unstructuredDeploy, err = kclient.ConvertK8sResourceToUnstructured(deployment) + if err != nil { + return isInnerLoopDeployed, resources, fmt.Errorf("failed to parse the resource %q: %q; cause: %w", kclient.DeploymentKind, deploymentName, err) + } + resources = append(resources, unstructuredDeploy) } - resources = append(resources, unstructuredDeploy) } - // Outer Loop - // Parse the devfile for outerloop K8s resources - localResources, err := libdevfile.ListKubernetesComponents(devfileObj, filepath.Dir(devfileObj.Ctx.GetAbsPath())) - if err != nil { - return isInnerLoopDeployed, resources, fmt.Errorf("failed to gather resources for deletion: %w", err) - } - for _, lr := range localResources { - var gvr *meta.RESTMapping - gvr, err = do.kubeClient.GetRestMappingFromUnstructured(lr) + if mode == odolabels.ComponentDeployMode || mode == odolabels.ComponentAnyMode { + // Outer Loop + // Parse the devfile for outerloop K8s resources + localResources, err := libdevfile.ListKubernetesComponents(devfileObj, filepath.Dir(devfileObj.Ctx.GetAbsPath())) if err != nil { - continue + return isInnerLoopDeployed, resources, fmt.Errorf("failed to gather resources for deletion: %w", err) } - // Try to fetch the resource from the cluster; if it exists, append it to the resources list - var cr *unstructured.Unstructured - cr, err = do.kubeClient.GetDynamicResource(gvr.Resource, lr.GetName()) - if err != nil { - continue + for _, lr := range localResources { + var gvr *meta.RESTMapping + gvr, err = do.kubeClient.GetRestMappingFromUnstructured(lr) + if err != nil { + continue + } + // Try to fetch the resource from the cluster; if it exists, append it to the resources list + var cr *unstructured.Unstructured + cr, err = do.kubeClient.GetDynamicResource(gvr.Resource, lr.GetName()) + if err != nil { + continue + } + resources = append(resources, *cr) } - resources = append(resources, *cr) } + return isInnerLoopDeployed, resources, nil } diff --git a/pkg/component/delete/delete_test.go b/pkg/component/delete/delete_test.go index c039d55d53b..ed165aec382 100644 --- a/pkg/component/delete/delete_test.go +++ b/pkg/component/delete/delete_test.go @@ -17,6 +17,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/redhat-developer/odo/pkg/kclient" + "github.com/redhat-developer/odo/pkg/labels" odolabels "github.com/redhat-developer/odo/pkg/labels" odoTestingUtil "github.com/redhat-developer/odo/pkg/testingutil" "github.com/redhat-developer/odo/pkg/util" @@ -465,7 +466,7 @@ func TestDeleteComponentClient_ListResourcesToDeleteFromDevfile(t *testing.T) { do := DeleteComponentClient{ kubeClient: tt.fields.kubeClient(ctrl), } - gotIsInnerLoopDeployed, gotResources, err := do.ListResourcesToDeleteFromDevfile(tt.args.devfileObj, tt.args.appName) + gotIsInnerLoopDeployed, gotResources, err := do.ListResourcesToDeleteFromDevfile(tt.args.devfileObj, tt.args.appName, labels.ComponentAnyMode) if (err != nil) != tt.wantErr { t.Errorf("ListResourcesToDeleteFromDevfile() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/component/delete/interface.go b/pkg/component/delete/interface.go index 9715d8c496a..0d0a9c6e853 100644 --- a/pkg/component/delete/interface.go +++ b/pkg/component/delete/interface.go @@ -15,5 +15,6 @@ type Client interface { ExecutePreStopEvents(devfileObj parser.DevfileObj, appName string) error // ListResourcesToDeleteFromDevfile parses all the devfile components and returns a list of resources that are present on the cluster that can be deleted, // and a bool that indicates if the devfile component has been pushed to the innerloop - ListResourcesToDeleteFromDevfile(devfileObj parser.DevfileObj, appName string) (bool, []unstructured.Unstructured, error) + // the mode indicates which component to list, either Dev, Deploy or Any (using constant labels.Component*Mode) + ListResourcesToDeleteFromDevfile(devfileObj parser.DevfileObj, appName string, mode string) (bool, []unstructured.Unstructured, error) } diff --git a/pkg/component/delete/mock.go b/pkg/component/delete/mock.go index 5ee1ce1624a..786f8649fd4 100644 --- a/pkg/component/delete/mock.go +++ b/pkg/component/delete/mock.go @@ -79,9 +79,9 @@ func (mr *MockClientMockRecorder) ListClusterResourcesToDelete(componentName, na } // ListResourcesToDeleteFromDevfile mocks base method. -func (m *MockClient) ListResourcesToDeleteFromDevfile(devfileObj parser.DevfileObj, appName string) (bool, []unstructured.Unstructured, error) { +func (m *MockClient) ListResourcesToDeleteFromDevfile(devfileObj parser.DevfileObj, appName, mode string) (bool, []unstructured.Unstructured, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListResourcesToDeleteFromDevfile", devfileObj, appName) + ret := m.ctrl.Call(m, "ListResourcesToDeleteFromDevfile", devfileObj, appName, mode) ret0, _ := ret[0].(bool) ret1, _ := ret[1].([]unstructured.Unstructured) ret2, _ := ret[2].(error) @@ -89,7 +89,7 @@ func (m *MockClient) ListResourcesToDeleteFromDevfile(devfileObj parser.DevfileO } // ListResourcesToDeleteFromDevfile indicates an expected call of ListResourcesToDeleteFromDevfile. -func (mr *MockClientMockRecorder) ListResourcesToDeleteFromDevfile(devfileObj, appName interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) ListResourcesToDeleteFromDevfile(devfileObj, appName, mode interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResourcesToDeleteFromDevfile", reflect.TypeOf((*MockClient)(nil).ListResourcesToDeleteFromDevfile), devfileObj, appName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResourcesToDeleteFromDevfile", reflect.TypeOf((*MockClient)(nil).ListResourcesToDeleteFromDevfile), devfileObj, appName, mode) } diff --git a/pkg/odo/cli/delete/component/component.go b/pkg/odo/cli/delete/component/component.go index 6253a1d8231..576467ac35a 100644 --- a/pkg/odo/cli/delete/component/component.go +++ b/pkg/odo/cli/delete/component/component.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ktemplates "k8s.io/kubectl/pkg/util/templates" + "github.com/redhat-developer/odo/pkg/labels" "github.com/redhat-developer/odo/pkg/log" "github.com/redhat-developer/odo/pkg/odo/cli/ui" "github.com/redhat-developer/odo/pkg/odo/cmdline" @@ -124,7 +125,7 @@ func (o *ComponentOptions) deleteDevfileComponent() error { appName := "app" log.Info("Searching resources to delete, please wait...") - isInnerLoopDeployed, devfileResources, err := o.clientset.DeleteClient.ListResourcesToDeleteFromDevfile(devfileObj, appName) + isInnerLoopDeployed, devfileResources, err := o.clientset.DeleteClient.ListResourcesToDeleteFromDevfile(devfileObj, appName, labels.ComponentAnyMode) if err != nil { return err } diff --git a/pkg/odo/cli/delete/component/component_test.go b/pkg/odo/cli/delete/component/component_test.go index 38d70ed1904..85fcbaf5a9a 100644 --- a/pkg/odo/cli/delete/component/component_test.go +++ b/pkg/odo/cli/delete/component/component_test.go @@ -18,6 +18,7 @@ import ( _delete "github.com/redhat-developer/odo/pkg/component/delete" "github.com/redhat-developer/odo/pkg/envinfo" "github.com/redhat-developer/odo/pkg/kclient" + "github.com/redhat-developer/odo/pkg/labels" "github.com/redhat-developer/odo/pkg/odo/cmdline" "github.com/redhat-developer/odo/pkg/odo/genericclioptions" "github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset" @@ -129,7 +130,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) { name: "deleting a component with access to devfile", deleteClient: func(ctrl *gomock.Controller) _delete.Client { deleteClient := _delete.NewMockClient(ctrl) - deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName).Return(true, resources, nil) + deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName, labels.ComponentAnyMode).Return(true, resources, nil) deleteClient.EXPECT().ListClusterResourcesToDelete(compName, projectName).Return(resources, nil) deleteClient.EXPECT().ExecutePreStopEvents(gomock.Any(), gomock.Any()).Return(nil) deleteClient.EXPECT().DeleteResources(resources, false).Return([]unstructured.Unstructured{}) @@ -144,7 +145,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) { name: "deleting a component should not fail even if ExecutePreStopEvents fails", deleteClient: func(ctrl *gomock.Controller) _delete.Client { deleteClient := _delete.NewMockClient(ctrl) - deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName).Return(true, resources, nil) + deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName, labels.ComponentAnyMode).Return(true, resources, nil) deleteClient.EXPECT().ListClusterResourcesToDelete(compName, projectName).Return(resources, nil) deleteClient.EXPECT().ExecutePreStopEvents(gomock.Any(), appName).Return(errors.New("some error")) deleteClient.EXPECT().DeleteResources(resources, false).Return(nil) @@ -159,7 +160,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) { name: "deleting a component should fail if ListResourcesToDeleteFromDevfile fails", deleteClient: func(ctrl *gomock.Controller) _delete.Client { deleteClient := _delete.NewMockClient(ctrl) - deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName).Return(false, nil, errors.New("some error")) + deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName, labels.ComponentAnyMode).Return(false, nil, errors.New("some error")) return deleteClient }, fields: fields{ @@ -171,7 +172,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) { name: "deleting a component should be aborted if forceFlag is not passed", deleteClient: func(ctrl *gomock.Controller) _delete.Client { deleteClient := _delete.NewMockClient(ctrl) - deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName).Return(true, resources, nil) + deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName, labels.ComponentAnyMode).Return(true, resources, nil) return deleteClient }, fields: fields{ @@ -183,7 +184,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) { name: "nothing to delete", deleteClient: func(ctrl *gomock.Controller) _delete.Client { deleteClient := _delete.NewMockClient(ctrl) - deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName).Return(false, nil, nil) + deleteClient.EXPECT().ListResourcesToDeleteFromDevfile(gomock.Any(), appName, labels.ComponentAnyMode).Return(false, nil, nil) return deleteClient }, fields: fields{ diff --git a/pkg/odo/cli/dev/dev.go b/pkg/odo/cli/dev/dev.go index 4c8f148919d..92d0ede14d3 100644 --- a/pkg/odo/cli/dev/dev.go +++ b/pkg/odo/cli/dev/dev.go @@ -191,7 +191,7 @@ func (o *DevOptions) Run(ctx context.Context) (err error) { defer func() { if err != nil { - _ = o.clientset.WatchClient.Cleanup(devFileObj, log.GetStdout()) + _ = o.clientset.WatchClient.CleanupDevResources(devFileObj, log.GetStdout()) } }() @@ -254,7 +254,7 @@ func (o *DevOptions) Run(ctx context.Context) (err error) { if o.noWatchFlag { log.Finfof(log.GetStdout(), "\n"+watch.CtrlCMessage) <-o.ctx.Done() - err = o.clientset.WatchClient.Cleanup(devFileObj, log.GetStdout()) + err = o.clientset.WatchClient.CleanupDevResources(devFileObj, log.GetStdout()) } else { d := Handler{} err = o.clientset.DevClient.Watch(devFileObj, path, o.ignorePaths, o.out, &d, o.ctx, o.debugFlag) diff --git a/pkg/watch/interface.go b/pkg/watch/interface.go index 0ffbb06e55b..49c01467e50 100644 --- a/pkg/watch/interface.go +++ b/pkg/watch/interface.go @@ -11,6 +11,6 @@ type Client interface { // WatchAndPush watches the component under the context directory and triggers Push if there are any changes // It also listens on ctx's Done channel to trigger cleanup when indicated to do so WatchAndPush(out io.Writer, parameters WatchParameters, ctx context.Context) error - // Cleanup deletes the component created using the devfileObj and writes any outputs to out - Cleanup(devfileObj parser.DevfileObj, out io.Writer) error + // CleanupDevResources deletes the component created using the devfileObj and writes any outputs to out + CleanupDevResources(devfileObj parser.DevfileObj, out io.Writer) error } diff --git a/pkg/watch/mock.go b/pkg/watch/mock.go index 6cd1b7535a5..0a29c55959d 100644 --- a/pkg/watch/mock.go +++ b/pkg/watch/mock.go @@ -36,18 +36,18 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } -// Cleanup mocks base method. -func (m *MockClient) Cleanup(devfileObj parser.DevfileObj, out io.Writer) error { +// CleanupDevResources mocks base method. +func (m *MockClient) CleanupDevResources(devfileObj parser.DevfileObj, out io.Writer) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Cleanup", devfileObj, out) + ret := m.ctrl.Call(m, "CleanupDevResources", devfileObj, out) ret0, _ := ret[0].(error) return ret0 } -// Cleanup indicates an expected call of Cleanup. -func (mr *MockClientMockRecorder) Cleanup(devfileObj, out interface{}) *gomock.Call { +// CleanupDevResources indicates an expected call of CleanupDevResources. +func (mr *MockClientMockRecorder) CleanupDevResources(devfileObj, out interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cleanup", reflect.TypeOf((*MockClient)(nil).Cleanup), devfileObj, out) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupDevResources", reflect.TypeOf((*MockClient)(nil).CleanupDevResources), devfileObj, out) } // WatchAndPush mocks base method. diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index 724827bcde2..db3b765a246 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -10,6 +10,7 @@ import ( "github.com/devfile/library/pkg/devfile/parser" _delete "github.com/redhat-developer/odo/pkg/component/delete" + "github.com/redhat-developer/odo/pkg/labels" "github.com/redhat-developer/odo/pkg/state" "github.com/fsnotify/fsnotify" @@ -213,7 +214,7 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct printInfoMessage(out, parameters.Path) - return eventWatcher(ctx, watcher, parameters, out, evaluateFileChanges, processEvents, o.Cleanup) + return eventWatcher(ctx, watcher, parameters, out, evaluateFileChanges, processEvents, o.CleanupDevResources) } // eventWatcher loops till the context's Done channel indicates it to stop looping, at which point it performs cleanup. @@ -358,9 +359,9 @@ func processEvents(changedFiles, deletedPaths []string, parameters WatchParamete } } -func (o *WatchClient) Cleanup(devfileObj parser.DevfileObj, out io.Writer) error { +func (o *WatchClient) CleanupDevResources(devfileObj parser.DevfileObj, out io.Writer) error { fmt.Fprintln(out, "Cleaning resources, please wait") - isInnerLoopDeployed, resources, err := o.deleteClient.ListResourcesToDeleteFromDevfile(devfileObj, "app") + isInnerLoopDeployed, resources, err := o.deleteClient.ListResourcesToDeleteFromDevfile(devfileObj, "app", labels.ComponentDevMode) if err != nil { fmt.Fprintf(out, "failed to delete inner loop resources: %v", err) return err From 289d2db16b15d44424ebe0cfd8d9bf6ec79a31a3 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 18 May 2022 14:48:27 +0200 Subject: [PATCH 3/5] doc --- .../versioned_docs/version-3.0.0/command-reference/dev.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md b/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md index 827985c4758..914d52c41e8 100644 --- a/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md +++ b/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md @@ -41,6 +41,9 @@ In the above example, three things have happened: * `odo` has port-forwarded your application for local accessability * `odo` will watch for changes in the current directory and rebuild the application when changes are detected +You can press Ctrl-c at any time to terminate the development session. The command can take a few moment to terminate, as it +will first delete all resources deployed into the cluster for this session before to terminate. + ## Devfile (Advanced Usage) From 497607bc36813dd47c9e88666642a9e0ba9793b3 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Thu, 19 May 2022 08:10:39 +0200 Subject: [PATCH 4/5] Update pkg/component/delete/delete.go Co-authored-by: Dharmit Shah --- pkg/component/delete/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/component/delete/delete.go b/pkg/component/delete/delete.go index 6e5b0a1d72a..8f4018ef4c8 100644 --- a/pkg/component/delete/delete.go +++ b/pkg/component/delete/delete.go @@ -96,7 +96,7 @@ func (do DeleteComponentClient) ListResourcesToDeleteFromDevfile(devfileObj pars var deploymentName string deploymentName, err = util.NamespaceKubernetesObject(componentName, appName) if err != nil { - return isInnerLoopDeployed, resources, fmt.Errorf("failed to get the resource %q name for component %q; cause: %w", kclient.DeploymentKind, deploymentName, err) + return isInnerLoopDeployed, resources, fmt.Errorf("failed to get the resource %q name for component %q; cause: %w", kclient.DeploymentKind, componentName, err) } deployment, err := do.kubeClient.GetDeploymentByName(deploymentName) From b20751392b3bed58c47aadf548361b59f56b0c9a Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Thu, 19 May 2022 12:38:03 +0200 Subject: [PATCH 5/5] Review --- .../versioned_docs/version-3.0.0/command-reference/dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md b/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md index 914d52c41e8..01ec75658d3 100644 --- a/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md +++ b/docs/website/versioned_docs/version-3.0.0/command-reference/dev.md @@ -42,7 +42,7 @@ In the above example, three things have happened: * `odo` will watch for changes in the current directory and rebuild the application when changes are detected You can press Ctrl-c at any time to terminate the development session. The command can take a few moment to terminate, as it -will first delete all resources deployed into the cluster for this session before to terminate. +will first delete all resources deployed into the cluster for this session before terminating. ## Devfile (Advanced Usage)