mirror of
				https://github.com/redhat-developer/odo.git
				synced 2025-10-19 03:06:19 +03:00 
			
		
		
		
	'odo watch' support for devfile components (#2737)
* Move file watch to its own package Signed-off-by: John Collier <John.J.Collier@ibm.com> * Use parameter struct for odo push Signed-off-by: John Collier <John.J.Collier@ibm.com> * Implement odo watch for devfiles Signed-off-by: John Collier <John.J.Collier@ibm.com> * Clean up and tests Signed-off-by: John Collier <John.J.Collier@ibm.com> * Un-export addRecursiveWatch Signed-off-by: John Collier <John.J.Collier@ibm.com> * Move devfile tests into own package and add more watch tests Signed-off-by: John Collier <John.J.Collier@ibm.com> * Fix go sec error Signed-off-by: John Collier <John.J.Collier@ibm.com> * Address review comments Signed-off-by: John Collier <John.J.Collier@ibm.com> * Properly apply ignores for devfile watching Signed-off-by: John Collier <John.J.Collier@ibm.com> * Properly apply ignores for `odo push` as well Signed-off-by: John Collier <John.J.Collier@ibm.com>
This commit is contained in:
		| @@ -119,6 +119,7 @@ jobs: | ||||
|         - travis_wait make test-cmd-devfile-catalog | ||||
|         - travis_wait make test-cmd-devfile-create | ||||
|         - travis_wait make test-cmd-devfile-push | ||||
|         - travis_wait make test-cmd-devfile-watch | ||||
|         - odo logout | ||||
|  | ||||
|     - <<: *base-test | ||||
|   | ||||
							
								
								
									
										12
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Makefile
									
									
									
									
									
								
							| @@ -206,8 +206,13 @@ test-cmd-devfile-create: | ||||
| # Run odo push devfile command tests | ||||
| .PHONY: test-cmd-devfile-push | ||||
| test-cmd-devfile-push: | ||||
| 	ginkgo $(GINKGO_FLAGS) -focus="odo devfile push command tests" tests/integration/ | ||||
| 	ginkgo $(GINKGO_FLAGS) -focus="odo devfile push command tests" tests/integration/devfile/ | ||||
|  | ||||
| # Run odo devfile watch command tests | ||||
| .PHONY: test-cmd-devfile-watch | ||||
| test-cmd-devfile-watch: | ||||
| 	ginkgo $(GINKGO_FLAGS) -focus="odo devfile watch command tests" tests/integration/devfile/ | ||||
| 	 | ||||
| # Run odo storage command tests | ||||
| .PHONY: test-cmd-storage | ||||
| test-cmd-storage: | ||||
| @@ -234,6 +239,11 @@ test-cmd-debug: | ||||
| test-integration: | ||||
| 	ginkgo $(GINKGO_FLAGS) tests/integration/ | ||||
|  | ||||
| # Run devfile integration tests | ||||
| .PHONY: test-integration-devfile | ||||
| test-integration-devfile: | ||||
| 	ginkgo $(GINKGO_FLAGS) tests/integration/devfile/ | ||||
|  | ||||
| # Run command's integration tests which are depend on service catalog enabled cluster. | ||||
| # Only service and link command tests are the part of this test run | ||||
| .PHONY: test-integration-service-catalog | ||||
|   | ||||
| @@ -2,7 +2,8 @@ package common | ||||
|  | ||||
| // ComponentAdapter defines the functions that platform-specific adapters must implement | ||||
| type ComponentAdapter interface { | ||||
| 	Push(path string, ignoredFiles []string, forceBuild bool, globExps []string) error | ||||
| 	Push(parameters PushParameters) error | ||||
| 	DoesComponentExist(cmpName string) bool | ||||
| } | ||||
|  | ||||
| // StorageAdapter defines the storage functions that platform-specific adapters must implement | ||||
|   | ||||
| @@ -22,3 +22,12 @@ type Storage struct { | ||||
| 	Name   string | ||||
| 	Volume DevfileVolume | ||||
| } | ||||
|  | ||||
| // PushParameters is a struct containing the parameters to be used when pushing to a devfile component | ||||
| type PushParameters struct { | ||||
| 	Path              string   // Path refers to the parent folder containing the source code to push up to a component | ||||
| 	WatchFiles        []string // Optional: WatchFiles is the list of changed files detected by odo watch. If empty or nil, odo will check .odo/odo-file-index.json to determine changed files | ||||
| 	WatchDeletedFiles []string // Optional: WatchDeletedFiles is the list of deleted files detected by odo watch. If empty or nil, odo will check .odo/odo-file-index.json to determine deleted files | ||||
| 	IgnoredFiles      []string // IgnoredFiles is the list of files to not push up to a component | ||||
| 	ForceBuild        bool     // ForceBuild determines whether or not to push all of the files up to a component or just files that have changed, added or removed. | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| package adapters | ||||
|  | ||||
| import "github.com/openshift/odo/pkg/devfile/adapters/common" | ||||
|  | ||||
| type PlatformAdapter interface { | ||||
| 	Push(path string, ignoredFiles []string, forceBuild bool, globExps []string) error | ||||
| 	Push(parameters common.PushParameters) error | ||||
| 	DoesComponentExist(cmpName string) bool | ||||
| } | ||||
|   | ||||
| @@ -27,12 +27,17 @@ func New(adapterContext common.AdapterContext, client kclient.Client) Adapter { | ||||
| } | ||||
|  | ||||
| // Push creates Kubernetes resources that correspond to the devfile if they don't already exist | ||||
| func (k Adapter) Push(path string, ignoredFiles []string, forceBuild bool, globExps []string) error { | ||||
| func (k Adapter) Push(parameters common.PushParameters) error { | ||||
|  | ||||
| 	err := k.componentAdapter.Push(path, ignoredFiles, forceBuild, globExps) | ||||
| 	err := k.componentAdapter.Push(parameters) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "Failed to create the component") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DoesComponentExist returns true if a component with the specified name exists | ||||
| func (k Adapter) DoesComponentExist(cmpName string) bool { | ||||
| 	return k.componentAdapter.DoesComponentExist(cmpName) | ||||
| } | ||||
|   | ||||
| @@ -39,8 +39,9 @@ type Adapter struct { | ||||
|  | ||||
| // Push updates the component if a matching component exists or creates one if it doesn't exist | ||||
| // Once the component has started, it will sync the source code to it. | ||||
| func (a Adapter) Push(path string, ignoredFiles []string, forceBuild bool, globExps []string) (err error) { | ||||
| func (a Adapter) Push(parameters common.PushParameters) (err error) { | ||||
| 	componentExists := utils.ComponentExists(a.Client, a.ComponentName) | ||||
| 	globExps := util.GetAbsGlobExps(parameters.Path, parameters.IgnoredFiles) | ||||
|  | ||||
| 	deletedFiles := []string{} | ||||
| 	changedFiles := []string{} | ||||
| @@ -59,8 +60,9 @@ func (a Adapter) Push(path string, ignoredFiles []string, forceBuild bool, globE | ||||
| 	// Sync source code to the component | ||||
| 	// If syncing for the first time, sync the entire source directory | ||||
| 	// If syncing to an already running component, sync the deltas | ||||
| 	if !forceBuild { | ||||
| 		absIgnoreRules := util.GetAbsGlobExps(path, ignoredFiles) | ||||
| 	// If syncing from an odo watch process, skip this step, as we already have the list of changed and deleted files. | ||||
| 	if !parameters.ForceBuild && len(parameters.WatchFiles) == 0 && len(parameters.WatchDeletedFiles) == 0 { | ||||
| 		absIgnoreRules := util.GetAbsGlobExps(parameters.Path, parameters.IgnoredFiles) | ||||
|  | ||||
| 		spinner := log.NewStatus(log.GetStdout()) | ||||
| 		defer spinner.End(true) | ||||
| @@ -73,7 +75,7 @@ func (a Adapter) Push(path string, ignoredFiles []string, forceBuild bool, globE | ||||
| 		} | ||||
|  | ||||
| 		// Before running the indexer, make sure the .odo folder exists (or else the index file will not get created) | ||||
| 		odoFolder := filepath.Join(path, ".odo") | ||||
| 		odoFolder := filepath.Join(parameters.Path, ".odo") | ||||
| 		if _, err := os.Stat(odoFolder); os.IsNotExist(err) { | ||||
| 			err = os.Mkdir(odoFolder, 0750) | ||||
| 			if err != nil { | ||||
| @@ -82,7 +84,7 @@ func (a Adapter) Push(path string, ignoredFiles []string, forceBuild bool, globE | ||||
| 		} | ||||
|  | ||||
| 		// run the indexer and find the modified/added/deleted/renamed files | ||||
| 		filesChanged, filesDeleted, err := util.RunIndexer(path, absIgnoreRules) | ||||
| 		filesChanged, filesDeleted, err := util.RunIndexer(parameters.Path, absIgnoreRules) | ||||
| 		spinner.End(true) | ||||
|  | ||||
| 		if err != nil { | ||||
| @@ -97,7 +99,7 @@ func (a Adapter) Push(path string, ignoredFiles []string, forceBuild bool, globE | ||||
|  | ||||
| 			// Remove the relative file directory from the list of deleted files | ||||
| 			// in order to make the changes correctly within the Kubernetes pod | ||||
| 			deletedFiles, err = util.RemoveRelativePathFromFiles(filesDeletedFiltered, path) | ||||
| 			deletedFiles, err = util.RemoveRelativePathFromFiles(filesDeletedFiltered, parameters.Path) | ||||
| 			if err != nil { | ||||
| 				return errors.Wrap(err, "unable to remove relative path from list of changed/deleted files") | ||||
| 			} | ||||
| @@ -110,14 +112,17 @@ func (a Adapter) Push(path string, ignoredFiles []string, forceBuild bool, globE | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 	} else if len(parameters.WatchFiles) > 0 || len(parameters.WatchDeletedFiles) > 0 { | ||||
| 		changedFiles = parameters.WatchFiles | ||||
| 		deletedFiles = parameters.WatchDeletedFiles | ||||
| 	} | ||||
|  | ||||
| 	if forceBuild || !componentExists { | ||||
| 	if parameters.ForceBuild || !componentExists { | ||||
| 		isForcePush = true | ||||
| 	} | ||||
|  | ||||
| 	// Sync the local source code to the component | ||||
| 	err = a.pushLocal(path, | ||||
| 	err = a.pushLocal(parameters.Path, | ||||
| 		changedFiles, | ||||
| 		deletedFiles, | ||||
| 		isForcePush, | ||||
| @@ -130,6 +135,11 @@ func (a Adapter) Push(path string, ignoredFiles []string, forceBuild bool, globE | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DoesComponentExist returns true if a component with the specified name exists, false otherwise | ||||
| func (a Adapter) DoesComponentExist(cmpName string) bool { | ||||
| 	return utils.ComponentExists(a.Client, cmpName) | ||||
| } | ||||
|  | ||||
| func (a Adapter) createOrUpdateComponent(componentExists bool) (err error) { | ||||
| 	componentName := a.ComponentName | ||||
|  | ||||
| @@ -207,6 +217,7 @@ func (a Adapter) createOrUpdateComponent(componentExists bool) (err error) { | ||||
| 	glog.V(3).Infof("The component name is %v", componentName) | ||||
|  | ||||
| 	if utils.ComponentExists(a.Client, componentName) { | ||||
| 		// If the component already exists, get the resource version of the deploy before updating | ||||
| 		glog.V(3).Info("The component already exists, attempting to update it") | ||||
| 		deployment, err := a.Client.UpdateDeployment(*deploymentSpec) | ||||
| 		if err != nil { | ||||
|   | ||||
| @@ -391,3 +391,67 @@ func TestGetCmdToDeleteFiles(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDoesComponentExist(t *testing.T) { | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name             string | ||||
| 		componentType    versionsCommon.DevfileComponentType | ||||
| 		componentName    string | ||||
| 		getComponentName string | ||||
| 		want             bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:             "Case 1: Valid component name", | ||||
| 			componentType:    versionsCommon.DevfileComponentTypeDockerimage, | ||||
| 			componentName:    "test-name", | ||||
| 			getComponentName: "test-name", | ||||
| 			want:             true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:             "Case 2: Non-existent component name", | ||||
| 			componentType:    versionsCommon.DevfileComponentTypeDockerimage, | ||||
| 			componentName:    "test-name", | ||||
| 			getComponentName: "fake-component", | ||||
| 			want:             false, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			devObj := devfile.DevfileObj{ | ||||
| 				Data: testingutil.TestDevfileData{ | ||||
| 					ComponentType: tt.componentType, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			adapterCtx := adaptersCommon.AdapterContext{ | ||||
| 				ComponentName: tt.componentName, | ||||
| 				Devfile:       devObj, | ||||
| 			} | ||||
|  | ||||
| 			fkclient, fkclientset := kclient.FakeNew() | ||||
| 			fkWatch := watch.NewFake() | ||||
|  | ||||
| 			fkclientset.Kubernetes.PrependWatchReactor("pods", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) { | ||||
| 				return true, fkWatch, nil | ||||
| 			}) | ||||
|  | ||||
| 			// DoesComponentExist requires an already started component, so start it. | ||||
| 			componentAdapter := New(adapterCtx, *fkclient) | ||||
| 			err := componentAdapter.createOrUpdateComponent(false) | ||||
|  | ||||
| 			// Checks for unexpected error cases | ||||
| 			if err != nil { | ||||
| 				t.Errorf("component adapter start unexpected error %v", err) | ||||
| 			} | ||||
|  | ||||
| 			// Verify that a comopnent with the specified name exists | ||||
| 			componentExists := componentAdapter.DoesComponentExist(tt.getComponentName) | ||||
| 			if componentExists != tt.want { | ||||
| 				t.Errorf("expected %v, actual %v", tt.want, componentExists) | ||||
| 			} | ||||
|  | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -7,10 +7,12 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/openshift/odo/pkg/devfile" | ||||
| 	"github.com/openshift/odo/pkg/odo/genericclioptions" | ||||
| 	"github.com/openshift/odo/pkg/util" | ||||
| 	"github.com/pkg/errors" | ||||
|  | ||||
| 	"github.com/openshift/odo/pkg/devfile/adapters" | ||||
| 	"github.com/openshift/odo/pkg/devfile/adapters/common" | ||||
| 	"github.com/openshift/odo/pkg/devfile/adapters/kubernetes" | ||||
| 	"github.com/openshift/odo/pkg/log" | ||||
| ) | ||||
| @@ -50,6 +52,12 @@ func (po *PushOptions) DevfilePush() (err error) { | ||||
| 		return errors.Wrap(err, "unable to get source path") | ||||
| 	} | ||||
|  | ||||
| 	// Apply ignore information | ||||
| 	err = genericclioptions.ApplyIgnore(&po.ignores, po.sourcePath) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "unable to apply ignore information") | ||||
| 	} | ||||
|  | ||||
| 	spinner := log.SpinnerNoSpin(fmt.Sprintf("Push devfile component %s", componentName)) | ||||
| 	defer spinner.End(false) | ||||
|  | ||||
| @@ -62,8 +70,14 @@ func (po *PushOptions) DevfilePush() (err error) { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	pushParams := common.PushParameters{ | ||||
| 		Path:         po.sourcePath, | ||||
| 		IgnoredFiles: po.ignores, | ||||
| 		ForceBuild:   po.forceBuild, | ||||
| 	} | ||||
|  | ||||
| 	// Start or update the component | ||||
| 	err = devfileHandler.Push(po.sourcePath, po.ignores, po.forceBuild, util.GetAbsGlobExps(po.sourcePath, po.ignores)) | ||||
| 	err = devfileHandler.Push(pushParams) | ||||
| 	if err != nil { | ||||
| 		log.Errorf( | ||||
| 			"Failed to start component with name %s.\nError: %v", | ||||
|   | ||||
| @@ -3,12 +3,17 @@ package component | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/openshift/odo/pkg/config" | ||||
| 	"github.com/openshift/odo/pkg/devfile" | ||||
| 	"github.com/openshift/odo/pkg/devfile/adapters" | ||||
| 	"github.com/openshift/odo/pkg/devfile/adapters/kubernetes" | ||||
| 	"github.com/openshift/odo/pkg/occlient" | ||||
| 	appCmd "github.com/openshift/odo/pkg/odo/cli/application" | ||||
| 	projectCmd "github.com/openshift/odo/pkg/odo/cli/project" | ||||
| 	"github.com/openshift/odo/pkg/odo/util/completion" | ||||
| 	"github.com/openshift/odo/pkg/odo/util/experimental" | ||||
| 	"github.com/pkg/errors" | ||||
| 	ktemplates "k8s.io/kubernetes/pkg/kubectl/util/templates" | ||||
|  | ||||
| @@ -18,6 +23,7 @@ import ( | ||||
| 	"github.com/openshift/odo/pkg/component" | ||||
| 	odoutil "github.com/openshift/odo/pkg/odo/util" | ||||
| 	"github.com/openshift/odo/pkg/util" | ||||
| 	"github.com/openshift/odo/pkg/watch" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| @@ -43,6 +49,11 @@ type WatchOptions struct { | ||||
| 	componentContext string | ||||
| 	client           *occlient.Client | ||||
|  | ||||
| 	componentName  string | ||||
| 	devfilePath    string | ||||
| 	namespace      string | ||||
| 	devfileHandler adapters.PlatformAdapter | ||||
|  | ||||
| 	*genericclioptions.Context | ||||
| } | ||||
|  | ||||
| @@ -53,6 +64,40 @@ func NewWatchOptions() *WatchOptions { | ||||
|  | ||||
| // Complete completes watch args | ||||
| func (wo *WatchOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) { | ||||
| 	// if experimental mode is enabled and devfile is present | ||||
| 	if experimental.IsExperimentalModeEnabled() && util.CheckPathExists(wo.devfilePath) { | ||||
| 		// Set the source path to either the context or current working directory (if context not set) | ||||
| 		wo.sourcePath, err = util.GetAbsPath(filepath.Dir(wo.componentContext)) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrap(err, "unable to get source path") | ||||
| 		} | ||||
|  | ||||
| 		// Apply ignore information | ||||
| 		err = genericclioptions.ApplyIgnore(&wo.ignores, wo.sourcePath) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrap(err, "unable to apply ignore information") | ||||
| 		} | ||||
|  | ||||
| 		// Get the component name | ||||
| 		wo.componentName, err = getComponentName() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// Parse devfile | ||||
| 		devObj, err := devfile.Parse(wo.devfilePath) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		kc := kubernetes.KubernetesContext{ | ||||
| 			Namespace: wo.namespace, | ||||
| 		} | ||||
| 		wo.devfileHandler, err = adapters.NewPlatformAdapter(wo.componentName, devObj, kc) | ||||
|  | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Set the correct context | ||||
| 	wo.Context = genericclioptions.NewContextCreatingAppIfNeeded(cmd) | ||||
|  | ||||
| @@ -80,6 +125,24 @@ func (wo *WatchOptions) Complete(name string, cmd *cobra.Command, args []string) | ||||
| // Validate validates the watch parameters | ||||
| func (wo *WatchOptions) Validate() (err error) { | ||||
|  | ||||
| 	// Delay interval cannot be -ve | ||||
| 	if wo.delay < 0 { | ||||
| 		return fmt.Errorf("Delay cannot be lesser than 0 and delay=0 means changes will be pushed as soon as they are detected which can cause performance issues") | ||||
| 	} | ||||
| 	// Print a debug message warning user if delay is set to 0 | ||||
| 	if wo.delay == 0 { | ||||
| 		glog.V(4).Infof("delay=0 means changes will be pushed as soon as they are detected which can cause performance issues") | ||||
| 	} | ||||
|  | ||||
| 	// if experimental mode is enabled and devfile is present, return. The rest of the validation is for non-devfile components | ||||
| 	if experimental.IsExperimentalModeEnabled() && util.CheckPathExists(wo.devfilePath) { | ||||
| 		exists := wo.devfileHandler.DoesComponentExist(wo.componentName) | ||||
| 		if !exists { | ||||
| 			return fmt.Errorf("component does not exist. Please use `odo push` to create your component") | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// Validate source of component is either local source or binary path until git watch is supported | ||||
| 	if wo.sourceType != "binary" && wo.sourceType != "local" { | ||||
| 		return fmt.Errorf("Watch is supported by binary and local components only and source type of component %s is %s", | ||||
| @@ -92,15 +155,6 @@ func (wo *WatchOptions) Validate() (err error) { | ||||
| 		return errors.Wrapf(err, "Cannot watch %s", wo.sourcePath) | ||||
| 	} | ||||
|  | ||||
| 	// Delay interval cannot be -ve | ||||
| 	if wo.delay < 0 { | ||||
| 		return fmt.Errorf("Delay cannot be lesser than 0 and delay=0 means changes will be pushed as soon as they are detected which can cause performance issues") | ||||
| 	} | ||||
| 	// Print a debug message warning user if delay is set to 0 | ||||
| 	if wo.delay == 0 { | ||||
| 		glog.V(4).Infof("delay=0 means changes will be pushed as soon as they are detected which can cause performance issues") | ||||
| 	} | ||||
|  | ||||
| 	cmpName := wo.LocalConfigInfo.GetName() | ||||
| 	appName := wo.LocalConfigInfo.GetApplication() | ||||
| 	exists, err := component.Exists(wo.Client, cmpName, appName) | ||||
| @@ -108,17 +162,39 @@ func (wo *WatchOptions) Validate() (err error) { | ||||
| 		return | ||||
| 	} | ||||
| 	if !exists { | ||||
| 		return fmt.Errorf("component does not exist. Please use `odo push` to create you component") | ||||
| 		return fmt.Errorf("component does not exist. Please use `odo push` to create your component") | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // Run has the logic to perform the required actions as part of command | ||||
| func (wo *WatchOptions) Run() (err error) { | ||||
| 	err = component.WatchAndPush( | ||||
| 	// if experimental mode is enabled and devfile is present | ||||
| 	if experimental.IsExperimentalModeEnabled() && util.CheckPathExists(wo.devfilePath) { | ||||
|  | ||||
| 		err = watch.DevfileWatchAndPush( | ||||
| 			os.Stdout, | ||||
| 			watch.WatchParameters{ | ||||
| 				ComponentName:       wo.componentName, | ||||
| 				Path:                wo.sourcePath, | ||||
| 				FileIgnores:         util.GetAbsGlobExps(wo.sourcePath, wo.ignores), | ||||
| 				PushDiffDelay:       wo.delay, | ||||
| 				StartChan:           nil, | ||||
| 				ExtChan:             make(chan bool), | ||||
| 				DevfileWatchHandler: wo.devfileHandler.Push, | ||||
| 				Show:                wo.show, | ||||
| 			}, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrapf(err, "Error while trying to watch %s", wo.sourcePath) | ||||
| 		} | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = watch.WatchAndPush( | ||||
| 		wo.Context.Client, | ||||
| 		os.Stdout, | ||||
| 		component.WatchParameters{ | ||||
| 		watch.WatchParameters{ | ||||
| 			ComponentName:   wo.LocalConfigInfo.GetName(), | ||||
| 			ApplicationName: wo.Context.Application, | ||||
| 			Path:            wo.sourcePath, | ||||
| @@ -157,6 +233,13 @@ func NewCmdWatch(name, fullName string) *cobra.Command { | ||||
| 	watchCmd.Flags().IntVar(&wo.delay, "delay", 1, "Time in seconds between a detection of code change and push.delay=0 means changes will be pushed as soon as they are detected which can cause performance issues") | ||||
|  | ||||
| 	watchCmd.SetUsageTemplate(odoutil.CmdUsageTemplate) | ||||
|  | ||||
| 	// enable devfile flag if experimental mode is enabled | ||||
| 	if experimental.IsExperimentalModeEnabled() { | ||||
| 		watchCmd.Flags().StringVar(&wo.devfilePath, "devfile", "./devfile.yaml", "Path to a devfile.yaml") | ||||
| 		watchCmd.Flags().StringVar(&wo.namespace, "namespace", "", "Namespace to push the component to") | ||||
| 	} | ||||
|  | ||||
| 	// Adding context flag | ||||
| 	genericclioptions.AddContextFlag(watchCmd, &wo.componentContext) | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| package component | ||||
| package watch | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/openshift/odo/pkg/devfile/adapters/common" | ||||
| 	"github.com/openshift/odo/pkg/util" | ||||
| 
 | ||||
| 	"github.com/openshift/odo/pkg/occlient" | ||||
| @@ -29,6 +30,8 @@ type WatchParameters struct { | ||||
| 	FileIgnores []string | ||||
| 	// Custom function that can be used to push detected changes to remote pod. For more info about what each of the parameters to this function, please refer, pkg/component/component.go#PushLocal | ||||
| 	WatchHandler func(*occlient.Client, string, string, string, io.Writer, []string, []string, bool, []string, bool) error | ||||
| 	// Custom function that can be used to push detected changes to remote devfile pod. For more info about what each of the parameters to this function, please refer, pkg/component/component.go#PushLocal | ||||
| 	DevfileWatchHandler func(common.PushParameters) error | ||||
| 	// This is a channel added to signal readiness of the watch command to the external channel listeners | ||||
| 	StartChan chan bool | ||||
| 	// This is a channel added to terminate the watch command gracefully without passing SIGINT. "Stop" message on this channel terminates WatchAndPush function | ||||
| @@ -299,16 +302,42 @@ func WatchAndPush(client *occlient.Client, out io.Writer, parameters WatchParame | ||||
| 				} | ||||
| 				if fileInfo.IsDir() { | ||||
| 					glog.V(4).Infof("Copying files %s to pod", changedFiles) | ||||
| 					err = parameters.WatchHandler(client, parameters.ComponentName, parameters.ApplicationName, parameters.Path, out, changedFiles, deletedPaths, false, parameters.FileIgnores, parameters.Show) | ||||
| 					if parameters.DevfileWatchHandler != nil { | ||||
| 						pushParams := common.PushParameters{ | ||||
| 							Path:              parameters.Path, | ||||
| 							WatchFiles:        changedFiles, | ||||
| 							WatchDeletedFiles: deletedPaths, | ||||
| 							IgnoredFiles:      parameters.FileIgnores, | ||||
| 							ForceBuild:        false, | ||||
| 						} | ||||
| 
 | ||||
| 						err = parameters.DevfileWatchHandler(pushParams) | ||||
| 					} else { | ||||
| 						err = parameters.WatchHandler(client, parameters.ComponentName, parameters.ApplicationName, parameters.Path, out, changedFiles, deletedPaths, false, parameters.FileIgnores, parameters.Show) | ||||
| 					} | ||||
| 
 | ||||
| 				} else { | ||||
| 					pathDir := filepath.Dir(parameters.Path) | ||||
| 					glog.V(4).Infof("Copying file %s to pod", parameters.Path) | ||||
| 					err = parameters.WatchHandler(client, parameters.ComponentName, parameters.ApplicationName, pathDir, out, []string{parameters.Path}, deletedPaths, false, parameters.FileIgnores, parameters.Show) | ||||
| 					if parameters.DevfileWatchHandler != nil { | ||||
| 						pushParams := common.PushParameters{ | ||||
| 							Path:              pathDir, | ||||
| 							WatchFiles:        changedFiles, | ||||
| 							WatchDeletedFiles: deletedPaths, | ||||
| 							IgnoredFiles:      parameters.FileIgnores, | ||||
| 							ForceBuild:        false, | ||||
| 						} | ||||
| 
 | ||||
| 						err = parameters.DevfileWatchHandler(pushParams) | ||||
| 					} else { | ||||
| 						err = parameters.WatchHandler(client, parameters.ComponentName, parameters.ApplicationName, pathDir, out, []string{parameters.Path}, deletedPaths, false, parameters.FileIgnores, parameters.Show) | ||||
| 					} | ||||
| 
 | ||||
| 				} | ||||
| 				if err != nil { | ||||
| 					// Intentionally not exiting on error here. | ||||
| 					// We don't want to break watch when push failed, it might be fixed with the next change. | ||||
| 					glog.V(4).Infof("Error from PushLocal: %v", err) | ||||
| 					glog.V(4).Infof("Error from Push: %v", err) | ||||
| 				} | ||||
| 				dirty = false | ||||
| 				showWaitingMessage = true | ||||
| @@ -322,3 +351,9 @@ func WatchAndPush(client *occlient.Client, out io.Writer, parameters WatchParame | ||||
| 		<-ticker.C | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // DevfileWatchAndPush calls out to the WatchAndPush function. | ||||
| // As an occlient instance is not needed for devfile components, it sets it to nil | ||||
| func DevfileWatchAndPush(out io.Writer, parameters WatchParameters) error { | ||||
| 	return WatchAndPush(nil, out, parameters) | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| // +build !osx | ||||
| 
 | ||||
| package component | ||||
| package watch | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| @@ -16,7 +16,9 @@ import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/openshift/odo/pkg/devfile/adapters/common" | ||||
| 	"github.com/openshift/odo/pkg/occlient" | ||||
| 	"github.com/openshift/odo/pkg/odo/util/experimental" | ||||
| 	"github.com/openshift/odo/pkg/testingutil" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
| @@ -103,6 +105,49 @@ type mockPushParameters struct { | ||||
| 
 | ||||
| var mockPush mockPushParameters | ||||
| 
 | ||||
| // Mocks the devfile push function that's called when odo watch pushes to a component | ||||
| func mockDevfilePush(parameters common.PushParameters) error { | ||||
| 	muLock.Lock() | ||||
| 	defer muLock.Unlock() | ||||
| 
 | ||||
| 	for _, expChangedFile := range ExpectedChangedFiles { | ||||
| 		found := false | ||||
| 		// Verify every file in expected file changes to be actually observed to be changed | ||||
| 		// If found exactly same or different, return from PushLocal and signal exit for watch so that the watch terminates gracefully | ||||
| 		for _, gotChangedFile := range parameters.WatchFiles { | ||||
| 			wantedFileDetail := CompDirStructure[filepath.FromSlash(expChangedFile)] | ||||
| 			if filepath.Join(wantedFileDetail.FileParent, wantedFileDetail.FilePath) == gotChangedFile { | ||||
| 				found = true | ||||
| 			} | ||||
| 		} | ||||
| 		if !found { | ||||
| 			ExtChan <- true | ||||
| 			fmt.Printf("received %+v which is not same as expected list %+v", parameters.WatchFiles, strings.Join(ExpectedChangedFiles, ",")) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, deletedFile := range DeleteFiles { | ||||
| 		found := false | ||||
| 		// Verify every file in expected deleted file changes to be actually observed to be changed | ||||
| 		// If found exactly same or different, return from PushLocal and signal exit for watch so that the watch terminates gracefully | ||||
| 		for _, gotChangedFile := range parameters.WatchDeletedFiles { | ||||
| 			wantedFileDetail := CompDirStructure[filepath.FromSlash(deletedFile)] | ||||
| 			if filepath.Join(wantedFileDetail.FileParent, wantedFileDetail.FilePath) == filepath.Join(wantedFileDetail.FileParent, filepath.Base(gotChangedFile)) { | ||||
| 				found = true | ||||
| 			} | ||||
| 		} | ||||
| 		if !found { | ||||
| 			ExtChan <- true | ||||
| 			fmt.Printf("received deleted files: %+v which is not same as expected list %+v", parameters.WatchDeletedFiles, strings.Join(DeleteFiles, ",")) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	ExtChan <- true | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Mock PushLocal to collect changed files and compare against expected changed files | ||||
| func mockPushLocal(client *occlient.Client, componentName string, applicationName string, path string, out io.Writer, files []string, delFiles []string, isPushForce bool, globExps []string, show bool) error { | ||||
| 	muLock.Lock() | ||||
| @@ -785,3 +830,515 @@ func TestWatchAndPush(t *testing.T) { | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestDevfileWatchAndPush(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name              string | ||||
| 		path              string | ||||
| 		ignores           []string | ||||
| 		show              bool | ||||
| 		forcePush         bool | ||||
| 		delayInterval     int | ||||
| 		wantErr           bool | ||||
| 		want              []string | ||||
| 		wantDeleted       []string | ||||
| 		fileModifications []testingutil.FileProperties | ||||
| 		requiredFilePaths []testingutil.FileProperties | ||||
| 		setupEnv          func(componentName string, requiredFilePaths []testingutil.FileProperties) (string, map[string]testingutil.FileProperties, error) | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:          "Case 1: Valid watch with list of files to be ignored with a append event", | ||||
| 			path:          "fabric8-analytics-license-analysis", | ||||
| 			ignores:       []string{".git", "tests/", "LICENSE"}, | ||||
| 			delayInterval: 1, | ||||
| 			wantErr:       false, | ||||
| 			show:          false, | ||||
| 			forcePush:     false, | ||||
| 			requiredFilePaths: []testingutil.FileProperties{ | ||||
| 				{ | ||||
| 					FilePath:         "src", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "tests", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         ".git", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "LICENSE", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "main.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "test1.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 			}, | ||||
| 			fileModifications: []testingutil.FileProperties{ | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.APPEND, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "read_licenses.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "read_licenses.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.APPEND, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "test_read_licenses.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "tests", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.DELETE, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:        []string{"src/read_licenses.py", "__init__.py"}, | ||||
| 			wantDeleted: []string{}, | ||||
| 			setupEnv:    setUpF8AnalyticsComponentSrc, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "Case 2: Valid watch with list of files to be ignored with a append and a delete event", | ||||
| 			path:          "fabric8-analytics-license-analysis", | ||||
| 			ignores:       []string{".git", "tests/", "LICENSE"}, | ||||
| 			delayInterval: 1, | ||||
| 			wantErr:       false, | ||||
| 			show:          false, | ||||
| 			forcePush:     false, | ||||
| 			requiredFilePaths: []testingutil.FileProperties{ | ||||
| 				{ | ||||
| 					FilePath:         "src", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "tests", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         ".git", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "LICENSE", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "main.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "test1.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "read_licenses.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 			}, | ||||
| 			fileModifications: []testingutil.FileProperties{ | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.APPEND, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "read_licenses.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.DELETE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "test_read_licenses.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "tests", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.DELETE, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:        []string{"__init__.py"}, | ||||
| 			wantDeleted: []string{"src/read_licenses.py"}, | ||||
| 			setupEnv:    setUpF8AnalyticsComponentSrc, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "Case 3: Valid watch with list of files to be ignored with a create and a delete event", | ||||
| 			path:          "fabric8-analytics-license-analysis", | ||||
| 			ignores:       []string{".git", "tests/", "LICENSE"}, | ||||
| 			delayInterval: 1, | ||||
| 			wantErr:       false, | ||||
| 			show:          false, | ||||
| 			forcePush:     false, | ||||
| 			requiredFilePaths: []testingutil.FileProperties{ | ||||
| 				{ | ||||
| 					FilePath:         "src", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "tests", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         ".git", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "LICENSE", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "main.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "test1.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "read_licenses.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 			}, | ||||
| 			fileModifications: []testingutil.FileProperties{ | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "read_licenses.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.DELETE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "test_read_licenses.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "tests", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.DELETE, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:        []string{"__init__.py"}, | ||||
| 			wantDeleted: []string{"src/read_licenses.py"}, | ||||
| 			setupEnv:    setUpF8AnalyticsComponentSrc, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "Case 4: Valid watch with list of files to be ignored with a folder create event", | ||||
| 			path:          "fabric8-analytics-license-analysis", | ||||
| 			ignores:       []string{".git", "tests/", "LICENSE"}, | ||||
| 			delayInterval: 1, | ||||
| 			wantErr:       false, | ||||
| 			show:          false, | ||||
| 			forcePush:     false, | ||||
| 			requiredFilePaths: []testingutil.FileProperties{ | ||||
| 				{ | ||||
| 					FilePath:         "src", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "tests", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         ".git", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "LICENSE", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "main.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "test1.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "read_licenses.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 			}, | ||||
| 			fileModifications: []testingutil.FileProperties{ | ||||
| 				{ | ||||
| 					FilePath:         "__init__.py", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "read_licenses.py", | ||||
| 					FileParent:       "src", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.DELETE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "test_read_licenses.py", | ||||
| 					FileParent:       "tests", | ||||
| 					FileType:         testingutil.RegularFile, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "tests", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.DELETE, | ||||
| 				}, | ||||
| 				{ | ||||
| 					FilePath:         "bin", | ||||
| 					FileParent:       "", | ||||
| 					FileType:         testingutil.Directory, | ||||
| 					ModificationType: testingutil.CREATE, | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:        []string{"__init__.py"}, | ||||
| 			wantDeleted: []string{"src/read_licenses.py"}, | ||||
| 			setupEnv:    setUpF8AnalyticsComponentSrc, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		err := os.Setenv(experimental.OdoExperimentalEnv, "true") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("failed to set env %s. err: '%v'", experimental.OdoExperimentalEnv, err) | ||||
| 		} | ||||
| 		defer os.Unsetenv(experimental.OdoExperimentalEnv) | ||||
| 
 | ||||
| 		ExtChan = make(chan bool) | ||||
| 		StartChan = make(chan bool) | ||||
| 		t.Log("Running test: ", tt.name) | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			mockPush = mockPushParameters{ | ||||
| 				path:        tt.path, | ||||
| 				isForcePush: tt.forcePush, | ||||
| 				globExps:    tt.ignores, | ||||
| 				show:        tt.show, | ||||
| 			} | ||||
| 
 | ||||
| 			ExpectedChangedFiles = tt.want | ||||
| 			DeleteFiles = tt.wantDeleted | ||||
| 			// Create mock component source | ||||
| 			basePath, dirStructure, err := tt.setupEnv(tt.path, tt.requiredFilePaths) | ||||
| 			CompDirStructure = dirStructure | ||||
| 			if err != nil { | ||||
| 				t.Errorf("failed to setup test environment. Error %v", err) | ||||
| 			} | ||||
| 
 | ||||
| 			fkclient, _ := occlient.FakeNew() | ||||
| 
 | ||||
| 			// Clear all the created temporary files | ||||
| 			defer os.RemoveAll(basePath) | ||||
| 			t.Logf("Done with basePath creation and client init will trigger WatchAndPush and file modifications next...\n%+v\n", CompDirStructure) | ||||
| 
 | ||||
| 			go func() { | ||||
| 				t.Logf("Starting file simulations \n%+v\n", tt.fileModifications) | ||||
| 				// Simulating file modifications for watch to observe | ||||
| 				pingTimeout := time.After(time.Duration(1) * time.Minute) | ||||
| 				for { | ||||
| 					select { | ||||
| 					case startMsg := <-StartChan: | ||||
| 						if startMsg { | ||||
| 							for _, fileModification := range tt.fileModifications { | ||||
| 
 | ||||
| 								intendedFileRelPath := fileModification.FilePath | ||||
| 								if fileModification.FileParent != "" { | ||||
| 									intendedFileRelPath = filepath.Join(fileModification.FileParent, fileModification.FilePath) | ||||
| 								} | ||||
| 
 | ||||
| 								fileModification.FileParent = CompDirStructure[fileModification.FileParent].FilePath | ||||
| 								if _, ok := CompDirStructure[intendedFileRelPath]; ok { | ||||
| 									fileModification.FilePath = CompDirStructure[intendedFileRelPath].FilePath | ||||
| 								} | ||||
| 
 | ||||
| 								newFilePath, err := testingutil.SimulateFileModifications(basePath, fileModification) | ||||
| 								if err != nil { | ||||
| 									t.Errorf("CompDirStructure: %+v\nFileModification %+v\nError %v\n", CompDirStructure, fileModification, err) | ||||
| 								} | ||||
| 
 | ||||
| 								// If file operation is create, store even such modifications in dir structure for future references | ||||
| 								if _, ok := CompDirStructure[intendedFileRelPath]; !ok && fileModification.ModificationType == testingutil.CREATE { | ||||
| 									muLock.Lock() | ||||
| 									CompDirStructure[intendedFileRelPath] = testingutil.FileProperties{ | ||||
| 										FilePath:         filepath.Base(newFilePath), | ||||
| 										FileParent:       filepath.Dir(newFilePath), | ||||
| 										FileType:         testingutil.Directory, | ||||
| 										ModificationType: testingutil.CREATE, | ||||
| 									} | ||||
| 									muLock.Unlock() | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 						t.Logf("The CompDirStructure is \n%+v\n", CompDirStructure) | ||||
| 						return | ||||
| 					case <-pingTimeout: | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 			}() | ||||
| 
 | ||||
| 			// Start WatchAndPush, the unit tested function | ||||
| 			t.Logf("Starting WatchAndPush now\n") | ||||
| 			err = WatchAndPush( | ||||
| 				fkclient, | ||||
| 				new(bytes.Buffer), | ||||
| 				WatchParameters{ | ||||
| 					Path:                basePath, | ||||
| 					FileIgnores:         tt.ignores, | ||||
| 					PushDiffDelay:       tt.delayInterval, | ||||
| 					StartChan:           StartChan, | ||||
| 					ExtChan:             ExtChan, | ||||
| 					Show:                tt.show, | ||||
| 					DevfileWatchHandler: mockDevfilePush, | ||||
| 				}, | ||||
| 			) | ||||
| 			if err != nil && err != ErrUserRequestedWatchExit { | ||||
| 				t.Errorf("error in WatchAndPush %+v", err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -16,6 +16,7 @@ export CUSTOM_HOMEDIR=$ARTIFACTS_DIR | ||||
|  | ||||
| # Integration tests | ||||
| make test-integration | ||||
| make test-integration-devfile | ||||
| make test-cmd-login-logout | ||||
| make test-cmd-project | ||||
| make test-operator-hub | ||||
|   | ||||
| @@ -46,7 +46,19 @@ var _ = Describe("odo watch command tests", func() { | ||||
| 			helper.CopyExample(filepath.Join("source", "nodejs"), context) | ||||
| 			helper.CmdShouldPass("odo", "component", "create", "nodejs", "--project", project, "--context", context) | ||||
| 			output := helper.CmdShouldFail("odo", "watch", "--context", context) | ||||
| 			Expect(output).To(ContainSubstring("component does not exist. Please use `odo push` to create you component")) | ||||
| 			Expect(output).To(ContainSubstring("component does not exist. Please use `odo push` to create your component")) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	Context("when executing watch without pushing a devfile component", func() { | ||||
| 		It("should fail", func() { | ||||
| 			// Devfile push requires experimental mode to be set | ||||
| 			helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") | ||||
|  | ||||
| 			helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), context) | ||||
|  | ||||
| 			output := helper.CmdShouldFail("odo", "watch", "--devfile", filepath.Join(context, "devfile.yaml")) | ||||
| 			Expect(output).To(ContainSubstring("component does not exist. Please use `odo push` to create your component")) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| package integration | ||||
| package devfile | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| @@ -21,7 +21,7 @@ var _ = Describe("odo devfile push command tests", func() { | ||||
| 
 | ||||
| 	// TODO: all oc commands in all devfile related test should get replaced by kubectl | ||||
| 	// TODO: to goal is not to use "oc" | ||||
| 	oc = helper.NewOcRunner("oc") | ||||
| 	oc := helper.NewOcRunner("oc") | ||||
| 
 | ||||
| 	// This is run after every Spec (It) | ||||
| 	var _ = BeforeEach(func() { | ||||
							
								
								
									
										80
									
								
								tests/integration/devfile/cmd_devfile_watch_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								tests/integration/devfile/cmd_devfile_watch_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| package devfile | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
|  | ||||
| 	. "github.com/onsi/ginkgo" | ||||
| 	. "github.com/onsi/gomega" | ||||
|  | ||||
| 	"github.com/openshift/odo/tests/helper" | ||||
| ) | ||||
|  | ||||
| var _ = Describe("odo devfile watch command tests", func() { | ||||
| 	var project string | ||||
| 	var context string | ||||
| 	var currentWorkingDirectory string | ||||
|  | ||||
| 	// Setup up state for each test spec | ||||
| 	// create new project (not set as active) and new context directory for each test spec | ||||
| 	// This is run after every Spec (It) | ||||
| 	var _ = BeforeEach(func() { | ||||
| 		SetDefaultEventuallyTimeout(10 * time.Minute) | ||||
| 		project = helper.CreateRandProject() | ||||
| 		context = helper.CreateNewContext() | ||||
| 		currentWorkingDirectory = helper.Getwd() | ||||
| 		helper.Chdir(context) | ||||
| 		os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml")) | ||||
| 	}) | ||||
|  | ||||
| 	// Clean up after the test | ||||
| 	// This is run after every Spec (It) | ||||
| 	var _ = AfterEach(func() { | ||||
| 		helper.DeleteProject(project) | ||||
| 		helper.Chdir(currentWorkingDirectory) | ||||
| 		helper.DeleteDir(context) | ||||
| 		os.Unsetenv("GLOBALODOCONFIG") | ||||
| 	}) | ||||
|  | ||||
| 	Context("when running help for watch command", func() { | ||||
| 		It("should display the help", func() { | ||||
| 			// Devfile push requires experimental mode to be set | ||||
| 			helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") | ||||
|  | ||||
| 			appHelp := helper.CmdShouldPass("odo", "watch", "-h") | ||||
| 			Expect(appHelp).To(ContainSubstring("Watch for changes")) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	Context("when executing watch without pushing a devfile component", func() { | ||||
| 		It("should fail", func() { | ||||
| 			// Devfile push requires experimental mode to be set | ||||
| 			helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") | ||||
|  | ||||
| 			helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), context) | ||||
|  | ||||
| 			output := helper.CmdShouldFail("odo", "watch", "--devfile", filepath.Join(context, "devfile.yaml")) | ||||
| 			Expect(output).To(ContainSubstring("component does not exist. Please use `odo push` to create your component")) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	Context("when executing watch without a valid devfile", func() { | ||||
| 		It("should fail", func() { | ||||
| 			// Devfile push requires experimental mode to be set | ||||
| 			helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") | ||||
|  | ||||
| 			output := helper.CmdShouldFail("odo", "watch", "--devfile", "fake-devfile.yaml") | ||||
| 			Expect(output).To(ContainSubstring("The current directory does not represent an odo component")) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	Context("when executing odo watch with devfile flag without experimental mode", func() { | ||||
| 		It("should fail", func() { | ||||
| 			helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), context) | ||||
|  | ||||
| 			output := helper.CmdShouldFail("odo", "watch", "--devfile", filepath.Join(context, "devfile.yaml")) | ||||
| 			Expect(output).To(ContainSubstring("Error: unknown flag: --devfile")) | ||||
| 		}) | ||||
| 	}) | ||||
| }) | ||||
							
								
								
									
										15
									
								
								tests/integration/devfile/devfile_suite_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/integration/devfile/devfile_suite_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| package devfile | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	. "github.com/onsi/ginkgo" | ||||
| 	. "github.com/onsi/gomega" | ||||
| ) | ||||
|  | ||||
| func TestDevfiles(t *testing.T) { | ||||
| 	RegisterFailHandler(Fail) | ||||
| 	RunSpecs(t, "Devfile Suite") | ||||
| 	// Keep CustomReporters commented till https://github.com/onsi/ginkgo/issues/628 is fixed | ||||
| 	// RunSpecsWithDefaultAndCustomReporters(t, "Project Suite", []Reporter{reporter.JunitReport(t, "../../../reports")}) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 John Collier
					John Collier