'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:
John Collier
2020-03-28 06:01:51 -04:00
committed by GitHub
parent 0e0886f4a7
commit 1eb878ea2d
17 changed files with 935 additions and 34 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)
}
})
}
}

View File

@@ -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",

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
}
})
}
}

View File

@@ -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

View File

@@ -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"))
})
})

View File

@@ -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() {

View 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"))
})
})
})

View 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")})
}