diff --git a/.travis.yml b/.travis.yml index ed878b38019..d570cfef9b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -93,6 +93,7 @@ jobs: - travis_wait make test-cmd-url - travis_wait make test-cmd-devfile-url - travis_wait make test-cmd-debug + - travis_wait make test-cmd-devfile-debug - odo logout # Run service-catalog e2e tests diff --git a/Makefile b/Makefile index 3d6e8636e7a..fdac68028ff 100644 --- a/Makefile +++ b/Makefile @@ -233,6 +233,11 @@ test-cmd-url: test-cmd-devfile-url: ginkgo $(GINKGO_FLAGS) -focus="odo devfile url command tests" tests/integration/devfile/ +# Run odo debug devfile command tests +.PHONY: test-cmd-devfile-debug +test-cmd-devfile-debug: + ginkgo $(GINKGO_FLAGS) -focus="odo devfile debug command tests" tests/integration/devfile/ + # Run odo push docker devfile command tests .PHONY: test-cmd-docker-devfile-push test-cmd-docker-devfile-push: diff --git a/pkg/debug/info.go b/pkg/debug/info.go index a114815bb44..66cc78f0ffd 100644 --- a/pkg/debug/info.go +++ b/pkg/debug/info.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "github.com/golang/glog" - "github.com/openshift/odo/pkg/occlient" "github.com/openshift/odo/pkg/testingutil/filesystem" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "net" @@ -23,18 +22,23 @@ type OdoDebugFile struct { } type OdoDebugFileSpec struct { - App string `json:"app"` + App string `json:"app,omitempty"` DebugProcessID int `json:"debugProcessID"` RemotePort int `json:"remotePort"` LocalPort int `json:"localPort"` } // GetDebugInfoFilePath gets the file path of the debug info file -func GetDebugInfoFilePath(client *occlient.Client, componentName, appName string) string { +func GetDebugInfoFilePath(componentName, appName string, projectName string) string { tempDir := os.TempDir() debugFileSuffix := "odo-debug.json" - s := []string{client.Namespace, appName, componentName, debugFileSuffix} - debugFileName := strings.Join(s, "-") + var arr []string + if appName == "" { + arr = []string{projectName, componentName, debugFileSuffix} + } else { + arr = []string{projectName, appName, componentName, debugFileSuffix} + } + debugFileName := strings.Join(arr, "-") return filepath.Join(tempDir, debugFileName) } @@ -65,7 +69,7 @@ func createDebugInfoFile(f *DefaultPortForwarder, portPair string, fs filesystem }, ObjectMeta: metav1.ObjectMeta{ Name: f.componentName, - Namespace: f.client.Namespace, + Namespace: f.projectName, }, Spec: OdoDebugFileSpec{ App: f.appName, @@ -80,7 +84,7 @@ func createDebugInfoFile(f *DefaultPortForwarder, portPair string, fs filesystem } // writes the data to the debug info file - file, err := fs.OpenFile(GetDebugInfoFilePath(f.client, f.componentName, f.appName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + file, err := fs.OpenFile(GetDebugInfoFilePath(f.componentName, f.appName, f.projectName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { return err } @@ -101,7 +105,7 @@ func GetDebugInfo(f *DefaultPortForwarder) (OdoDebugFile, bool) { // returns true if debugging is running else false func getDebugInfo(f *DefaultPortForwarder, fs filesystem.Filesystem) (OdoDebugFile, bool) { // gets the debug info file path and reads/unmarshals it - debugInfoFilePath := GetDebugInfoFilePath(f.client, f.componentName, f.appName) + debugInfoFilePath := GetDebugInfoFilePath(f.componentName, f.appName, f.projectName) readFile, err := fs.ReadFile(debugInfoFilePath) if err != nil { glog.V(4).Infof("the debug %v is not present", debugInfoFilePath) diff --git a/pkg/debug/info_test.go b/pkg/debug/info_test.go index 7f2dce1b0e0..db728084555 100644 --- a/pkg/debug/info_test.go +++ b/pkg/debug/info_test.go @@ -7,7 +7,6 @@ import ( "reflect" "testing" - "github.com/openshift/odo/pkg/occlient" "github.com/openshift/odo/pkg/testingutil" "github.com/openshift/odo/pkg/testingutil/filesystem" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -59,6 +58,7 @@ func Test_createDebugInfoFile(t *testing.T) { defaultPortForwarder: &DefaultPortForwarder{ componentName: "nodejs-ex", appName: "app", + projectName: "testing-1", }, portPair: "5858:9001", fs: fs, @@ -88,6 +88,7 @@ func Test_createDebugInfoFile(t *testing.T) { defaultPortForwarder: &DefaultPortForwarder{ componentName: "nodejs-ex", appName: "app", + projectName: "testing-1", }, portPair: "5758:9004", fs: fs, @@ -115,12 +116,7 @@ func Test_createDebugInfoFile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Fake the client with the appropriate arguments - client, _ := occlient.FakeNew() - client.Namespace = "testing-1" - tt.args.defaultPortForwarder.client = client - - debugFilePath := GetDebugInfoFilePath(client, tt.args.defaultPortForwarder.componentName, tt.args.defaultPortForwarder.appName) + debugFilePath := GetDebugInfoFilePath(tt.args.defaultPortForwarder.componentName, tt.args.defaultPortForwarder.appName, tt.args.defaultPortForwarder.projectName) // create a already existing file if tt.alreadyExistFile { _, err := testingutil.MkFileWithContent(debugFilePath, "blah", fs) @@ -177,6 +173,7 @@ func Test_getDebugInfo(t *testing.T) { defaultPortForwarder: &DefaultPortForwarder{ appName: "app", componentName: "nodejs-ex", + projectName: "testing-1", }, fs: fs, }, @@ -222,6 +219,7 @@ func Test_getDebugInfo(t *testing.T) { defaultPortForwarder: &DefaultPortForwarder{ appName: "app", componentName: "nodejs-ex", + projectName: "testing-1", }, fs: fs, }, @@ -237,6 +235,7 @@ func Test_getDebugInfo(t *testing.T) { defaultPortForwarder: &DefaultPortForwarder{ appName: "app", componentName: "nodejs-ex", + projectName: "testing-1", }, fs: fs, }, @@ -267,6 +266,7 @@ func Test_getDebugInfo(t *testing.T) { defaultPortForwarder: &DefaultPortForwarder{ appName: "app", componentName: "nodejs-ex", + projectName: "testing-1", }, fs: fs, }, @@ -295,11 +295,6 @@ func Test_getDebugInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Fake the client with the appropriate arguments - client, _ := occlient.FakeNew() - client.Namespace = "testing-1" - tt.args.defaultPortForwarder.client = client - freePort, err := util.HttpGetFreePort() if err != nil { t.Errorf("error occured while getting a free port, cause: %v", err) @@ -313,7 +308,7 @@ func Test_getDebugInfo(t *testing.T) { tt.wantDebugFile.Spec.LocalPort = freePort } - odoDebugFilePath := GetDebugInfoFilePath(tt.args.defaultPortForwarder.client, tt.args.defaultPortForwarder.componentName, tt.args.defaultPortForwarder.appName) + odoDebugFilePath := GetDebugInfoFilePath(tt.args.defaultPortForwarder.componentName, tt.args.defaultPortForwarder.appName, tt.args.defaultPortForwarder.projectName) if tt.fileExists { fakeString, err := fakeOdoDebugFileString(tt.readDebugFile.TypeMeta, tt.readDebugFile.Spec.DebugProcessID, diff --git a/pkg/debug/portforward.go b/pkg/debug/portforward.go index 10df7ae47a7..e76fc37addb 100644 --- a/pkg/debug/portforward.go +++ b/pkg/debug/portforward.go @@ -1,16 +1,14 @@ package debug import ( + "github.com/openshift/odo/pkg/kclient" "github.com/openshift/odo/pkg/occlient" - - componentlabels "github.com/openshift/odo/pkg/component/labels" + "k8s.io/client-go/rest" "fmt" "net/http" "github.com/openshift/odo/pkg/log" - "github.com/openshift/odo/pkg/util" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" k8sgenclioptions "k8s.io/cli-runtime/pkg/genericclioptions" @@ -20,18 +18,22 @@ import ( // DefaultPortForwarder implements the SPDY based port forwarder type DefaultPortForwarder struct { - client *occlient.Client + client *occlient.Client + kClient *kclient.Client k8sgenclioptions.IOStreams componentName string appName string + projectName string } -func NewDefaultPortForwarder(componentName, appName string, client *occlient.Client, streams k8sgenclioptions.IOStreams) *DefaultPortForwarder { +func NewDefaultPortForwarder(componentName, appName string, projectName string, client *occlient.Client, kClient *kclient.Client, streams k8sgenclioptions.IOStreams) *DefaultPortForwarder { return &DefaultPortForwarder{ client: client, + kClient: kClient, IOStreams: streams, componentName: componentName, appName: appName, + projectName: projectName, } } @@ -39,15 +41,31 @@ func NewDefaultPortForwarder(componentName, appName string, client *occlient.Cli // portPair is a pair of port in format "localPort:RemotePort" that is to be forwarded // stop Chan is used to stop port forwarding // ready Chan is used to signal failure to the channel receiver -func (f *DefaultPortForwarder) ForwardPorts(portPair string, stopChan, readyChan chan struct{}) error { - conf, err := f.client.KubeConfig.ClientConfig() - if err != nil { - return err - } +func (f *DefaultPortForwarder) ForwardPorts(portPair string, stopChan, readyChan chan struct{}, isExperimental bool) error { + var pod *corev1.Pod + var conf *rest.Config + var err error - pod, err := f.getPodUsingComponentName() - if err != nil { - return err + if f.kClient != nil && isExperimental { + conf, err = f.kClient.KubeConfig.ClientConfig() + if err != nil { + return err + } + + pod, err = f.kClient.GetPodUsingComponentName(f.componentName) + if err != nil { + return err + } + } else { + conf, err = f.client.KubeConfig.ClientConfig() + if err != nil { + return err + } + + pod, err = f.client.GetPodUsingComponentName(f.componentName, f.appName) + if err != nil { + return err + } } if pod.Status.Phase != corev1.PodRunning { @@ -58,7 +76,14 @@ func (f *DefaultPortForwarder) ForwardPorts(portPair string, stopChan, readyChan if err != nil { return err } - req := f.client.BuildPortForwardReq(pod.Name) + + var req *rest.Request + if f.kClient != nil && isExperimental { + req = f.kClient.GeneratePortForwardReq(pod.Name) + } else { + req = f.client.BuildPortForwardReq(pod.Name) + } + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL()) fw, err := portforward.New(dialer, []string{portPair}, stopChan, readyChan, f.Out, f.ErrOut) if err != nil { @@ -67,16 +92,3 @@ func (f *DefaultPortForwarder) ForwardPorts(portPair string, stopChan, readyChan log.Info("Started port forwarding at ports -", portPair) return fw.ForwardPorts() } - -func (f *DefaultPortForwarder) getPodUsingComponentName() (*corev1.Pod, error) { - componentLabels := componentlabels.GetLabels(f.componentName, f.appName, false) - componentSelector := util.ConvertLabelsToSelector(componentLabels) - dc, err := f.client.GetOneDeploymentConfigFromSelector(componentSelector) - if err != nil { - return nil, errors.Wrap(err, "unable to get deployment for component") - } - // Find Pod for component - podSelector := fmt.Sprintf("deploymentconfig=%s", dc.Name) - - return f.client.GetOnePodFromSelector(podSelector) -} diff --git a/pkg/devfile/adapters/common/command.go b/pkg/devfile/adapters/common/command.go index e7a5b8491f6..ceca63b3cc9 100644 --- a/pkg/devfile/adapters/common/command.go +++ b/pkg/devfile/adapters/common/command.go @@ -128,6 +128,19 @@ func GetBuildCommand(data data.DevfileData, devfileBuildCmd string) (buildComman return getCommand(data, string(DefaultDevfileBuildCommand), false) } +// GetDebugCommand iterates through the components in the devfile and returns the build command +func GetDebugCommand(data data.DevfileData, devfileDebugCmd string) (buildCommand common.DevfileCommand, err error) { + if devfileDebugCmd != "" { + // a build command was specified so if it is not found then it is an error + buildCommand, err = getCommand(data, devfileDebugCmd, true) + } else { + // a build command was not specified so if it is not found then it is not an error + buildCommand, err = getCommand(data, string(DefaultDevfileDebugCommand), false) + } + + return +} + // GetRunCommand iterates through the components in the devfile and returns the run command func GetRunCommand(data data.DevfileData, devfileRunCmd string) (runCommand common.DevfileCommand, err error) { if devfileRunCmd != "" { @@ -194,3 +207,25 @@ func ValidateAndGetPushDevfileCommands(data data.DevfileData, devfileInitCmd, de return pushDevfileCommands, nil } + +// ValidateAndGetDebugDevfileCommands validates the debug command +func ValidateAndGetDebugDevfileCommands(data data.DevfileData, devfileDebugCmd string) (pushDebugCommand common.DevfileCommand, err error) { + var emptyCommand common.DevfileCommand + + isDebugCommandValid := false + debugCommand, debugCmdErr := GetDebugCommand(data, devfileDebugCmd) + if debugCmdErr == nil && !reflect.DeepEqual(emptyCommand, debugCommand) { + isDebugCommandValid = true + glog.V(3).Infof("Debug command: %v", debugCommand.Name) + } + + if !isDebugCommandValid { + commandErrors := "" + if debugCmdErr != nil { + commandErrors += debugCmdErr.Error() + } + return common.DevfileCommand{}, fmt.Errorf(commandErrors) + } + + return debugCommand, nil +} diff --git a/pkg/devfile/adapters/common/command_test.go b/pkg/devfile/adapters/common/command_test.go index c6efc7052f3..1bb07ce6621 100644 --- a/pkg/devfile/adapters/common/command_test.go +++ b/pkg/devfile/adapters/common/command_test.go @@ -560,6 +560,84 @@ func TestGetBuildCommand(t *testing.T) { } +func TestGetDebugCommand(t *testing.T) { + + command := "ls -la" + component := "alias1" + workDir := "/" + validCommandType := common.DevfileCommandTypeExec + emptyString := "" + + var emptyCommand common.DevfileCommand + + tests := []struct { + name string + commandName string + commandActions []common.DevfileCommandAction + wantErr bool + }{ + { + name: "Case: Default Debug Command", + commandName: emptyString, + commandActions: []common.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + wantErr: false, + }, + { + name: "Case: Custom Debug Command", + commandName: "customdebugcommand", + commandActions: []common.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + wantErr: false, + }, + { + name: "Case: Missing Debug Command", + commandName: "customcommand123", + commandActions: []common.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + devObj := devfileParser.DevfileObj{ + Data: testingutil.TestDevfileData{ + CommandActions: tt.commandActions, + ComponentType: common.DevfileComponentTypeDockerimage, + }, + } + + command, err := GetDebugCommand(devObj.Data, tt.commandName) + + if !tt.wantErr == (err != nil) { + t.Errorf("TestGetDebugCommand: unexpected error for command \"%v\" expected: %v actual: %v", tt.commandName, tt.wantErr, err) + } else if !tt.wantErr && reflect.DeepEqual(emptyCommand, command) { + t.Errorf("TestGetDebugCommand: unexpected empty command returned for command: %v", tt.commandName) + } + + }) + } +} + func TestGetRunCommand(t *testing.T) { command := "ls -la" @@ -638,6 +716,76 @@ func TestGetRunCommand(t *testing.T) { } +func TestValidateAndGetDebugDevfileCommands(t *testing.T) { + + command := "ls -la" + component := "alias1" + workDir := "/" + validCommandType := common.DevfileCommandTypeExec + emptyString := "" + + actions := []common.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + } + + tests := []struct { + name string + debugCommand string + componentType common.DevfileComponentType + wantErr bool + }{ + { + name: "Case: Default Devfile Commands", + debugCommand: emptyString, + componentType: common.DevfileComponentTypeDockerimage, + wantErr: false, + }, + { + name: "Case: provided debug Command", + debugCommand: "customdebugcommand", + componentType: versionsCommon.DevfileComponentTypeDockerimage, + wantErr: false, + }, + { + name: "Case: invalid debug Command", + debugCommand: "invaliddebugcommand", + componentType: versionsCommon.DevfileComponentTypeDockerimage, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + devObj := devfileParser.DevfileObj{ + Data: testingutil.TestDevfileData{ + CommandActions: actions, + ComponentType: tt.componentType, + }, + } + + debugCommand, err := ValidateAndGetDebugDevfileCommands(devObj.Data, tt.debugCommand) + if !tt.wantErr == (err != nil) { + t.Errorf("TestValidateAndGetDebugDevfileCommands unexpected error when validating commands wantErr: %v err: %v", tt.wantErr, err) + } else if tt.wantErr && err != nil { + return + } + + if tt.debugCommand == "" { + tt.debugCommand = "devdebug" + } + + if !reflect.DeepEqual(nil, debugCommand) && debugCommand.Name != tt.debugCommand { + t.Errorf("TestValidateAndGetDebugDevfileCommands name of debug command is wrong want: %v err: %v", tt.debugCommand, debugCommand.Name) + } + }) + } +} + func TestValidateAndGetPushDevfileCommands(t *testing.T) { command := "ls -la" diff --git a/pkg/devfile/adapters/common/types.go b/pkg/devfile/adapters/common/types.go index 8b95ee53dea..ccdc9b02d57 100644 --- a/pkg/devfile/adapters/common/types.go +++ b/pkg/devfile/adapters/common/types.go @@ -35,7 +35,10 @@ type PushParameters struct { DevfileInitCmd string // DevfileInitCmd takes the init command through the command line and overwrites devfile init command DevfileBuildCmd string // DevfileBuildCmd takes the build command through the command line and overwrites devfile build command DevfileRunCmd string // DevfileRunCmd takes the run command through the command line and overwrites devfile run command + DevfileDebugCmd string // DevfileDebugCmd takes the debug command through the command line and overwrites the devfile debug command EnvSpecificInfo envinfo.EnvSpecificInfo // EnvSpecificInfo contains infomation of env.yaml file + Debug bool // Runs the component in debug mode + DebugPort int // Port used for remote debugging } // SyncParameters is a struct containing the parameters to be used when syncing a devfile component diff --git a/pkg/devfile/adapters/common/utils.go b/pkg/devfile/adapters/common/utils.go index 96204b1aba2..a96de75e5bb 100644 --- a/pkg/devfile/adapters/common/utils.go +++ b/pkg/devfile/adapters/common/utils.go @@ -24,6 +24,9 @@ const ( // DefaultDevfileRunCommand is a predefined devfile command for run DefaultDevfileRunCommand PredefinedDevfileCommands = "devrun" + // DefaultDevfileDebugCommand is a predefined devfile command for build + DefaultDevfileDebugCommand PredefinedDevfileCommands = "devdebug" + // SupervisordInitContainerName The init container name for supervisord SupervisordInitContainerName = "copy-supervisord" @@ -67,6 +70,15 @@ const ( // EnvOdoCommandRun is the env defined in the runtime component container which holds the run command to be executed EnvOdoCommandRun = "ODO_COMMAND_RUN" + // EnvOdoCommandDebugWorkingDir is the env defined in the runtime component container which holds the work dir for the debug command + EnvOdoCommandDebugWorkingDir = "ODO_COMMAND_DEBUG_WORKING_DIR" + + // EnvOdoCommandDebug is the env defined in the runtime component container which holds the debug command to be executed + EnvOdoCommandDebug = "ODO_COMMAND_DEBUG" + + // EnvDebugPort is the env defined in the runtime component container which holds the debug port for remote debugging + EnvDebugPort = "DEBUG_PORT" + // ShellExecutable is the shell executable ShellExecutable = "/bin/sh" diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index 99d73387f08..af8c7e15732 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter.go @@ -37,9 +37,11 @@ func New(adapterContext common.AdapterContext, client kclient.Client) Adapter { type Adapter struct { Client kclient.Client common.AdapterContext - devfileInitCmd string - devfileBuildCmd string - devfileRunCmd string + devfileInitCmd string + devfileBuildCmd string + devfileRunCmd string + devfileDebugCmd string + devfileDebugPort int } // Push updates the component if a matching component exists or creates one if it doesn't exist @@ -50,6 +52,8 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) { a.devfileInitCmd = parameters.DevfileInitCmd a.devfileBuildCmd = parameters.DevfileBuildCmd a.devfileRunCmd = parameters.DevfileRunCmd + a.devfileDebugCmd = parameters.DevfileDebugCmd + a.devfileDebugPort = parameters.DebugPort podChanged := false var podName string @@ -74,6 +78,19 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) { s.End(true) log.Infof("\nCreating Kubernetes resources for component %s", a.ComponentName) + + if parameters.Debug { + pushDevfileDebugCommands, err := common.ValidateAndGetDebugDevfileCommands(a.Devfile.Data, a.devfileDebugCmd) + if err != nil { + return fmt.Errorf("debug command is not valid") + } + pushDevfileCommands = append(pushDevfileCommands, pushDevfileDebugCommands) + } + + if parameters.Debug { + parameters.ForceBuild = true + } + err = a.createOrUpdateComponent(componentExists) if err != nil { return errors.Wrap(err, "unable to create or update component") @@ -126,7 +143,7 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) { if execRequired { log.Infof("\nExecuting devfile commands for component %s", a.ComponentName) - err = a.execDevfile(pushDevfileCommands, componentExists, parameters.Show, pod.GetName(), pod.Spec.Containers) + err = a.execDevfile(pushDevfileCommands, componentExists, parameters.Show, pod.GetName(), pod.Spec.Containers, parameters.Debug) if err != nil { return err } @@ -156,7 +173,7 @@ func (a Adapter) createOrUpdateComponent(componentExists bool) (err error) { return fmt.Errorf("No valid components found in the devfile") } - containers, err = utils.UpdateContainersWithSupervisord(a.Devfile, containers, a.devfileRunCmd) + containers, err = utils.UpdateContainersWithSupervisord(a.Devfile, containers, a.devfileRunCmd, a.devfileDebugCmd, a.devfileDebugPort) if err != nil { return err } @@ -305,7 +322,7 @@ func (a Adapter) waitAndGetComponentPod(hideSpinner bool) (*corev1.Pod, error) { // Executes all the commands from the devfile in order: init and build - which are both optional, and a compulsary run. // Init only runs once when the component is created. -func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand, componentExists, show bool, podName string, containers []corev1.Container) (err error) { +func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand, componentExists, show bool, podName string, containers []corev1.Container, isDebug bool) (err error) { // If nothing has been passed, then the devfile is missing the required run command if len(pushDevfileCommands) == 0 { return errors.New(fmt.Sprint("error executing devfile commands - there should be at least 1 command")) @@ -321,9 +338,18 @@ func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand commandOrder = append( commandOrder, common.CommandNames{DefaultName: string(common.DefaultDevfileBuildCommand), AdapterName: a.devfileBuildCmd}, - common.CommandNames{DefaultName: string(common.DefaultDevfileRunCommand), AdapterName: a.devfileRunCmd}, ) + if isDebug { + commandOrder = append(commandOrder, + common.CommandNames{DefaultName: string(common.DefaultDevfileDebugCommand), AdapterName: a.devfileDebugCmd}, + ) + } else { + commandOrder = append(commandOrder, + common.CommandNames{DefaultName: string(common.DefaultDevfileRunCommand), AdapterName: a.devfileRunCmd}, + ) + } + // Loop through each of the expected commands in the devfile for i, currentCommand := range commandOrder { // Loop through each of the command given from the devfile @@ -331,7 +357,7 @@ func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand // If the current command from the devfile is the currently expected command from the devfile if command.Name == currentCommand.DefaultName || command.Name == currentCommand.AdapterName { // If the current command is not the last command in the slice - // it is not expected to be the run command + // it is not expected to be the run or debug command if i < len(commandOrder)-1 { // Any exec command such as "Init" and "Build" @@ -349,8 +375,8 @@ func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand // If the current command is the last command in the slice // it is expected to be the run command - } else { - // Last command is "Run" + } else if !isDebug { + // Last command is "Run" if not in debug mode glog.V(4).Infof("Executing devfile command %v", command.Name) for _, action := range command.Actions { @@ -377,6 +403,31 @@ func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand err = exec.ExecuteDevfileRunAction(&a.Client, action, command.Name, compInfo, show) } + } else if isDebug { + // Always check for buildRequired is false, since the command may be iterated out of order and we always want to execute devBuild first if buildRequired is true. If buildRequired is false, then we don't need to build and we can execute the devRun command + glog.V(3).Infof("Executing devfile command %v", command.Name) + + for _, action := range command.Actions { + + // Check if the devfile run component containers have supervisord as the entrypoint. + // Start the supervisord if the odo component does not exist + if !componentExists { + err = a.InitRunContainerSupervisord(*action.Component, podName, containers) + if err != nil { + return + } + } + + compInfo := common.ComponentInfo{ + ContainerName: *action.Component, + PodName: podName, + } + + err = exec.ExecuteDevfileDebugAction(&a.Client, action, command.Name, compInfo, show) + if err != nil { + return err + } + } } } } diff --git a/pkg/devfile/adapters/kubernetes/utils/utils.go b/pkg/devfile/adapters/kubernetes/utils/utils.go index 31ff45690d0..240bc3a63fa 100644 --- a/pkg/devfile/adapters/kubernetes/utils/utils.go +++ b/pkg/devfile/adapters/kubernetes/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "fmt" + "strconv" "strings" adaptersCommon "github.com/openshift/odo/pkg/devfile/adapters/common" @@ -109,18 +110,23 @@ func isEnvPresent(EnvVars []corev1.EnvVar, envVarName string) bool { // UpdateContainersWithSupervisord updates the run components entrypoint and volume mount // with supervisord if no entrypoint has been specified for the component in the devfile -func UpdateContainersWithSupervisord(devfileObj devfileParser.DevfileObj, containers []corev1.Container, devfileRunCmd string) ([]corev1.Container, error) { +func UpdateContainersWithSupervisord(devfileObj devfileParser.DevfileObj, containers []corev1.Container, devfileRunCmd string, devfileDebugCmd string, devfileDebugPort int) ([]corev1.Container, error) { runCommand, err := adaptersCommon.GetRunCommand(devfileObj.Data, devfileRunCmd) if err != nil { return nil, err } + debugCommand, err := adaptersCommon.GetDebugCommand(devfileObj.Data, devfileDebugCmd) + if err != nil { + return nil, err + } + for i, container := range containers { for _, action := range runCommand.Actions { // Check if the container belongs to a run command component if container.Name == *action.Component { - // If the run component container has no entrypoint and arguments, override the entrypoint with supervisord + // If the run component container has no entry point and arguments, override the entry point with supervisord if len(container.Command) == 0 && len(container.Args) == 0 { glog.V(3).Infof("Updating container %v entrypoint with supervisord", container.Name) container.Command = append(container.Command, adaptersCommon.SupervisordBinaryPath) @@ -159,6 +165,67 @@ func UpdateContainersWithSupervisord(devfileObj devfileParser.DevfileObj, contai containers[i] = container } } + + for _, action := range debugCommand.Actions { + // Check if the container belongs to a run command component + if container.Name == *action.Component { + // If the debug component container has no entry point and arguments, override the entry point with supervisord + if len(container.Command) == 0 && len(container.Args) == 0 { + glog.V(3).Infof("Updating container %v entrypoint with supervisord", container.Name) + container.Command = append(container.Command, adaptersCommon.SupervisordBinaryPath) + container.Args = append(container.Args, "-c", adaptersCommon.SupervisordConfFile) + } + + foundMountPath := false + for _, mounts := range container.VolumeMounts { + if mounts.Name == adaptersCommon.SupervisordVolumeName && mounts.MountPath == adaptersCommon.SupervisordMountPath { + foundMountPath = true + } + } + + if !foundMountPath { + // Always mount the supervisord volume in the run component container + glog.V(3).Infof("Updating container %v with supervisord volume mounts", container.Name) + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: adaptersCommon.SupervisordVolumeName, + MountPath: adaptersCommon.SupervisordMountPath, + }) + } + + // Update the run container's ENV for work dir and command + // only if the env var is not set in the devfile + // This is done, so supervisord can use it in it's program + if !isEnvPresent(container.Env, adaptersCommon.EnvOdoCommandDebug) { + glog.V(3).Infof("Updating container %v env with debug command", container.Name) + container.Env = append(container.Env, + corev1.EnvVar{ + Name: adaptersCommon.EnvOdoCommandDebug, + Value: *action.Command, + }) + } + + if !isEnvPresent(container.Env, adaptersCommon.EnvOdoCommandDebugWorkingDir) && action.Workdir != nil { + glog.V(3).Infof("Updating container %v env with debug command's workdir", container.Name) + container.Env = append(container.Env, + corev1.EnvVar{ + Name: adaptersCommon.EnvOdoCommandDebugWorkingDir, + Value: *action.Workdir, + }) + } + + if !isEnvPresent(container.Env, adaptersCommon.EnvDebugPort) && action.Workdir != nil { + glog.V(3).Infof("Updating container %v env with run command's workdir", container.Name) + container.Env = append(container.Env, + corev1.EnvVar{ + Name: adaptersCommon.EnvDebugPort, + Value: strconv.Itoa(devfileDebugPort), + }) + } + + // Update the containers array since the array is not a pointer to the container + containers[i] = container + } + } } return containers, nil diff --git a/pkg/devfile/adapters/kubernetes/utils/utils_test.go b/pkg/devfile/adapters/kubernetes/utils/utils_test.go index 8f12c3c8a78..551e55eef90 100644 --- a/pkg/devfile/adapters/kubernetes/utils/utils_test.go +++ b/pkg/devfile/adapters/kubernetes/utils/utils_test.go @@ -17,6 +17,10 @@ func TestUpdateContainersWithSupervisord(t *testing.T) { command := "ls -la" component := "alias1" + + debugCommand := "nodemon --inspect={DEBUG_PORT}" + debugComponent := "alias2" + image := "image1" workDir := "/root" validCommandType := common.DevfileCommandTypeExec @@ -29,6 +33,7 @@ func TestUpdateContainersWithSupervisord(t *testing.T) { tests := []struct { name string runCommand string + debugCommand string containers []corev1.Container commandActions []common.DevfileCommandAction componentType common.DevfileComponentType @@ -153,6 +158,103 @@ func TestUpdateContainersWithSupervisord(t *testing.T) { isSupervisordEntrypoint: true, wantErr: true, }, + + { + name: "Case: empty debug command", + runCommand: emptyString, + debugCommand: emptyString, + containers: []corev1.Container{ + { + Name: component, + Image: image, + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + }, + { + Name: debugComponent, + Image: image, + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + }, + }, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + { + Command: &debugCommand, + Component: &debugComponent, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + componentType: common.DevfileComponentTypeDockerimage, + isSupervisordEntrypoint: true, + wantErr: false, + }, + { + name: "Case: custom debug command", + runCommand: "", + debugCommand: "customdebugcommand", + containers: []corev1.Container{ + { + Name: debugComponent, + Image: image, + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + }, + }, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &debugCommand, + Component: &debugComponent, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + componentType: common.DevfileComponentTypeDockerimage, + isSupervisordEntrypoint: true, + wantErr: false, + }, + { + name: "Case: wrong custom debug command", + runCommand: "", + debugCommand: "customdebugcommand123", + containers: []corev1.Container{ + { + Name: component, + Image: image, + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + }, + { + Name: debugComponent, + Image: image, + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + }, + }, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + { + Command: &debugCommand, + Component: &debugComponent, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + componentType: common.DevfileComponentTypeDockerimage, + isSupervisordEntrypoint: true, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -163,19 +265,22 @@ func TestUpdateContainersWithSupervisord(t *testing.T) { }, } - containers, err := UpdateContainersWithSupervisord(devObj, tt.containers, tt.runCommand) + containers, err := UpdateContainersWithSupervisord(devObj, tt.containers, tt.runCommand, tt.debugCommand, 5858) if !tt.wantErr && err != nil { t.Errorf("TestUpdateContainersWithSupervisord unxpected error: %v", err) } else if tt.wantErr && err != nil { // return since we dont want to test anything further return + } else if tt.wantErr && err == nil { + t.Error("wanted error but got not error") } // Check if the supervisord volume has been mounted supervisordVolumeMountMatched := false envRunMatched := false envWorkDirMatched := false + envDebugMatched := false if tt.commandActions[0].Workdir == nil { // if workdir is not present, dont test for matching the env @@ -183,35 +288,39 @@ func TestUpdateContainersWithSupervisord(t *testing.T) { } for _, container := range containers { - if container.Name == component { - for _, volumeMount := range container.VolumeMounts { - if volumeMount.Name == adaptersCommon.SupervisordVolumeName && volumeMount.MountPath == adaptersCommon.SupervisordMountPath { - supervisordVolumeMountMatched = true + for _, testContainer := range tt.containers { + if container.Name == testContainer.Name { + for _, volumeMount := range container.VolumeMounts { + if volumeMount.Name == adaptersCommon.SupervisordVolumeName && volumeMount.MountPath == adaptersCommon.SupervisordMountPath { + supervisordVolumeMountMatched = true + } } - } - for _, envVar := range container.Env { - if envVar.Name == adaptersCommon.EnvOdoCommandRun && envVar.Value == *tt.commandActions[0].Command { - envRunMatched = true - } - if tt.commandActions[0].Workdir != nil && envVar.Name == adaptersCommon.EnvOdoCommandRunWorkingDir && envVar.Value == *tt.commandActions[0].Workdir { - envWorkDirMatched = true + for _, envVar := range container.Env { + if envVar.Name == adaptersCommon.EnvOdoCommandRun && envVar.Value == *tt.commandActions[0].Command { + envRunMatched = true + } + if tt.commandActions[0].Workdir != nil && envVar.Name == adaptersCommon.EnvOdoCommandRunWorkingDir && envVar.Value == *tt.commandActions[0].Workdir { + envWorkDirMatched = true + } + if envVar.Name == adaptersCommon.EnvOdoCommandDebug && envVar.Value == *tt.commandActions[0].Command { + envDebugMatched = true + } } - } - if tt.isSupervisordEntrypoint && (!reflect.DeepEqual(container.Command, supervisordCommand) || !reflect.DeepEqual(container.Args, supervisordArgs)) { - t.Errorf("TestUpdateContainersWithSupervisord error: commands and args mismatched for container %v, expected command: %v actual command: %v, expected args: %v actual args: %v", component, supervisordCommand, container.Command, supervisordArgs, container.Args) - } else if !tt.isSupervisordEntrypoint && (!reflect.DeepEqual(container.Command, defaultCommand) || !reflect.DeepEqual(container.Args, defaultArgs)) { - t.Errorf("TestUpdateContainersWithSupervisord error: commands and args mismatched for container %v, expected command: %v actual command: %v, expected args: %v actual args: %v", component, defaultCommand, container.Command, defaultArgs, container.Args) + if tt.isSupervisordEntrypoint && (!reflect.DeepEqual(container.Command, supervisordCommand) || !reflect.DeepEqual(container.Args, supervisordArgs)) { + t.Errorf("TestUpdateContainersWithSupervisord error: commands and args mismatched for container %v, expected command: %v actual command: %v, expected args: %v actual args: %v", component, supervisordCommand, container.Command, supervisordArgs, container.Args) + } else if !tt.isSupervisordEntrypoint && (!reflect.DeepEqual(container.Command, defaultCommand) || !reflect.DeepEqual(container.Args, defaultArgs)) { + t.Errorf("TestUpdateContainersWithSupervisord error: commands and args mismatched for container %v, expected command: %v actual command: %v, expected args: %v actual args: %v", component, defaultCommand, container.Command, defaultArgs, container.Args) + } } } } - if !supervisordVolumeMountMatched { t.Errorf("TestUpdateContainersWithSupervisord error: could not find supervisord volume mounts for container %v", component) } - if !envRunMatched || !envWorkDirMatched { + if !envRunMatched || !envWorkDirMatched || !envDebugMatched { t.Errorf("TestUpdateContainersWithSupervisord error: could not find env vars for supervisord in container %v, found command env: %v, found work dir env: %v", component, envRunMatched, envWorkDirMatched) } }) diff --git a/pkg/envinfo/envinfo.go b/pkg/envinfo/envinfo.go index 9663a738b30..d9924290ac9 100644 --- a/pkg/envinfo/envinfo.go +++ b/pkg/envinfo/envinfo.go @@ -17,6 +17,9 @@ import ( const ( envInfoEnvName = "ENVINFO" envInfoFileName = "env.yaml" + + // DefaultDebugPort is the default port used for debugging on remote pod + DefaultDebugPort = 5858 ) // ComponentSettings holds all component related information @@ -24,6 +27,9 @@ type ComponentSettings struct { Name string `yaml:"Name,omitempty"` Namespace string `yaml:"Namespace,omitempty"` URL *[]EnvInfoURL `yaml:"Url,omitempty"` + + // DebugPort controls the port used by the pod to run the debugging agent on + DebugPort *int `yaml:"DebugPort,omitempty"` } // URLKind is an enum to indicate the type of the URL i.e ingress/route @@ -273,6 +279,14 @@ func (ei *EnvInfo) GetName() string { return ei.componentSettings.Name } +// GetDebugPort returns the DebugPort, returns default if nil +func (ei *EnvInfo) GetDebugPort() int { + if ei.componentSettings.DebugPort == nil { + return DefaultDebugPort + } + return *ei.componentSettings.DebugPort +} + // GetNamespace returns component namespace func (ei *EnvInfo) GetNamespace() string { if ei.componentSettings.Namespace == "" { diff --git a/pkg/exec/devfile.go b/pkg/exec/devfile.go index 9116e8b76a4..0277b30b65b 100644 --- a/pkg/exec/devfile.go +++ b/pkg/exec/devfile.go @@ -96,3 +96,35 @@ func ExecuteDevfileRunActionWithoutRestart(client ExecClient, action common.Devf return nil } + +// ExecuteDevfileDebugAction executes the devfile debug command action using the supervisord devdebug program +func ExecuteDevfileDebugAction(client ExecClient, action common.DevfileCommandAction, commandName string, compInfo adaptersCommon.ComponentInfo, show bool) error { + var s *log.Status + + // Exec the supervisord ctl stop and start for the devrun program + type devDebugExecutable struct { + command []string + } + devDebugExecs := []devDebugExecutable{ + { + command: []string{adaptersCommon.SupervisordBinaryPath, adaptersCommon.SupervisordCtlSubCommand, "stop", "all"}, + }, + { + command: []string{adaptersCommon.SupervisordBinaryPath, adaptersCommon.SupervisordCtlSubCommand, "start", string(adaptersCommon.DefaultDevfileDebugCommand)}, + }, + } + + s = log.Spinnerf("Executing %s command %q", commandName, *action.Command) + defer s.End(false) + + for _, devDebugExec := range devDebugExecs { + + err := ExecuteCommand(client, compInfo, devDebugExec.command, show) + if err != nil { + return errors.Wrapf(err, "unable to execute the run command") + } + } + s.End(true) + + return nil +} diff --git a/pkg/kclient/fakeclient.go b/pkg/kclient/fakeclient.go index 3e88b1d0978..726a84e7808 100644 --- a/pkg/kclient/fakeclient.go +++ b/pkg/kclient/fakeclient.go @@ -30,7 +30,8 @@ func FakeNew() (*Client, *FakeClientset) { func FakePodStatus(status corev1.PodPhase, podName string) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: podName, + Name: podName, + Labels: map[string]string{}, }, Status: corev1.PodStatus{ Phase: status, diff --git a/pkg/kclient/generators.go b/pkg/kclient/generators.go index ab99888eda5..0f55d404498 100644 --- a/pkg/kclient/generators.go +++ b/pkg/kclient/generators.go @@ -2,6 +2,7 @@ package kclient import ( "github.com/openshift/odo/pkg/devfile/adapters/common" + "k8s.io/client-go/rest" // api resource types @@ -249,3 +250,13 @@ func GenerateOwnerReference(deployment *appsv1.Deployment) metav1.OwnerReference return ownerReference } + +// GeneratePortForwardReq builds a port forward request +func (c *Client) GeneratePortForwardReq(podName string) *rest.Request { + return c.KubeClient.CoreV1().RESTClient(). + Post(). + Resource("pods"). + Namespace(c.Namespace). + Name(podName). + SubResource("portforward") +} diff --git a/pkg/kclient/pods.go b/pkg/kclient/pods.go index 6533dcd4ffa..fd5479d0910 100644 --- a/pkg/kclient/pods.go +++ b/pkg/kclient/pods.go @@ -2,6 +2,7 @@ package kclient import ( "bytes" + "fmt" "io" "strings" "time" @@ -147,3 +148,27 @@ func (c *Client) ExtractProjectToComponent(compInfo common.ComponentInfo, target } return nil } + +// GetPodUsingComponentName gets a pod using the component name +func (c *Client) GetPodUsingComponentName(componentName string) (*corev1.Pod, error) { + podSelector := fmt.Sprintf("component=%s", componentName) + return c.GetOnePodFromSelector(podSelector) +} + +// GetOnePodFromSelector gets a pod from the selector +func (c *Client) GetOnePodFromSelector(selector string) (*corev1.Pod, error) { + pods, err := c.KubeClient.CoreV1().Pods(c.Namespace).List(metav1.ListOptions{ + LabelSelector: selector, + }) + if err != nil { + return nil, errors.Wrapf(err, "unable to get Pod for the selector: %v", selector) + } + numPods := len(pods.Items) + if numPods == 0 { + return nil, fmt.Errorf("no Pod was found for the selector: %v", selector) + } else if numPods > 1 { + return nil, fmt.Errorf("multiple Pods exist for the selector: %v. Only one must be present", selector) + } + + return &pods.Items[0], nil +} diff --git a/pkg/kclient/pods_test.go b/pkg/kclient/pods_test.go index 6e97c0235de..203b28f5e82 100644 --- a/pkg/kclient/pods_test.go +++ b/pkg/kclient/pods_test.go @@ -2,6 +2,8 @@ package kclient import ( "fmt" + "k8s.io/apimachinery/pkg/runtime" + "reflect" "testing" corev1 "k8s.io/api/core/v1" @@ -82,3 +84,125 @@ func TestWaitAndGetPod(t *testing.T) { }) } } + +func TestGetOnePodFromSelector(t *testing.T) { + fakePod := FakePodStatus(corev1.PodRunning, "nodejs") + fakePod.Labels["component"] = "nodejs" + + type args struct { + selector string + } + tests := []struct { + name string + args args + returnedPods *corev1.PodList + want *corev1.Pod + wantErr bool + }{ + { + name: "valid number of pods", + args: args{selector: fmt.Sprintf("component=%s", "nodejs")}, + returnedPods: &corev1.PodList{ + Items: []corev1.Pod{ + *fakePod, + }, + }, + want: fakePod, + wantErr: false, + }, + { + name: "zero pods", + args: args{selector: fmt.Sprintf("component=%s", "nodejs")}, + returnedPods: &corev1.PodList{ + Items: []corev1.Pod{}, + }, + want: &corev1.Pod{}, + wantErr: true, + }, + { + name: "mutiple pods", + args: args{selector: fmt.Sprintf("component=%s", "nodejs")}, + returnedPods: &corev1.PodList{ + Items: []corev1.Pod{ + *fakePod, + *fakePod, + }, + }, + want: &corev1.Pod{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + fkclient, fkclientset := FakeNew() + + fkclientset.Kubernetes.PrependReactor("list", "pods", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) { + if action.(ktesting.ListAction).GetListRestrictions().Labels.String() != fmt.Sprintf("component=%s", "nodejs") { + t.Errorf("list called with different selector want:%s, got:%s", fmt.Sprintf("component=%s", "nodejs"), action.(ktesting.ListAction).GetListRestrictions().Labels.String()) + } + return true, tt.returnedPods, nil + }) + + got, err := fkclient.GetOnePodFromSelector(tt.args.selector) + if (err != nil) != tt.wantErr { + t.Errorf("GetOnePodFromSelector() error = %v, wantErr %v", err, tt.wantErr) + return + } else if tt.wantErr && err != nil { + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetOnePodFromSelector() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetPodUsingComponentName(t *testing.T) { + fakePod := FakePodStatus(corev1.PodRunning, "nodejs") + fakePod.Labels["component"] = "nodejs" + + type args struct { + componentName string + } + tests := []struct { + name string + args args + want *corev1.Pod + wantErr bool + }{ + { + name: "list called with same component name", + args: args{ + componentName: "nodejs", + }, + want: fakePod, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fkclient, fkclientset := FakeNew() + + fkclientset.Kubernetes.PrependReactor("list", "pods", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) { + if action.(ktesting.ListAction).GetListRestrictions().Labels.String() != fmt.Sprintf("component=%s", tt.args.componentName) { + t.Errorf("list called with different selector want:%s, got:%s", fmt.Sprintf("component=%s", tt.args.componentName), action.(ktesting.ListAction).GetListRestrictions().Labels.String()) + } + return true, &corev1.PodList{ + Items: []corev1.Pod{ + *fakePod, + }, + }, nil + }) + + got, err := fkclient.GetPodUsingComponentName(tt.args.componentName) + if (err != nil) != tt.wantErr { + t.Errorf("GetPodUsingComponentName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetPodUsingComponentName() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/occlient/pods.go b/pkg/occlient/pods.go new file mode 100644 index 00000000000..b5f7a689786 --- /dev/null +++ b/pkg/occlient/pods.go @@ -0,0 +1,22 @@ +package occlient + +import ( + "fmt" + componentlabels "github.com/openshift/odo/pkg/component/labels" + "github.com/openshift/odo/pkg/util" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" +) + +func (c *Client) GetPodUsingComponentName(componentName, appName string) (*corev1.Pod, error) { + componentLabels := componentlabels.GetLabels(componentName, appName, false) + componentSelector := util.ConvertLabelsToSelector(componentLabels) + dc, err := c.GetOneDeploymentConfigFromSelector(componentSelector) + if err != nil { + return nil, errors.Wrap(err, "unable to get deployment for component") + } + // Find Pod for component + podSelector := fmt.Sprintf("deploymentconfig=%s", dc.Name) + + return c.GetOnePodFromSelector(podSelector) +} diff --git a/pkg/odo/cli/component/devfile.go b/pkg/odo/cli/component/devfile.go index 06bd0d5b846..05c9c433c8f 100644 --- a/pkg/odo/cli/component/devfile.go +++ b/pkg/odo/cli/component/devfile.go @@ -82,6 +82,9 @@ func (po *PushOptions) DevfilePush() (err error) { DevfileInitCmd: strings.ToLower(po.devfileInitCommand), DevfileBuildCmd: strings.ToLower(po.devfileBuildCommand), DevfileRunCmd: strings.ToLower(po.devfileRunCommand), + DevfileDebugCmd: strings.ToLower(po.devfileDebugCommand), + Debug: po.debugRun, + DebugPort: po.EnvSpecificInfo.GetDebugPort(), } // Start or update the component diff --git a/pkg/odo/cli/component/push.go b/pkg/odo/cli/component/push.go index e882557b061..0457e17f31e 100644 --- a/pkg/odo/cli/component/push.go +++ b/pkg/odo/cli/component/push.go @@ -45,7 +45,10 @@ type PushOptions struct { devfileInitCommand string devfileBuildCommand string devfileRunCommand string - namespace string + devfileDebugCommand string + debugRun bool + + namespace string } // NewPushOptions returns new instance of PushOptions @@ -173,6 +176,8 @@ func NewCmdPush(name, fullName string) *cobra.Command { pushCmd.Flags().StringVar(&po.devfileInitCommand, "init-command", "", "Devfile Init Command to execute") pushCmd.Flags().StringVar(&po.devfileBuildCommand, "build-command", "", "Devfile Build Command to execute") pushCmd.Flags().StringVar(&po.devfileRunCommand, "run-command", "", "Devfile Run Command to execute") + pushCmd.Flags().BoolVar(&po.debugRun, "debug", false, "Runs the component in debug mode") + pushCmd.Flags().StringVar(&po.devfileDebugCommand, "debug-command", "", "Devfile Debug Command to execute") } //Adding `--project` flag diff --git a/pkg/odo/cli/debug/info.go b/pkg/odo/cli/debug/info.go index a05dfbda107..5f52151fea3 100644 --- a/pkg/odo/cli/debug/info.go +++ b/pkg/odo/cli/debug/info.go @@ -2,12 +2,12 @@ package debug import ( "fmt" - - "github.com/openshift/odo/pkg/config" "github.com/openshift/odo/pkg/debug" "github.com/openshift/odo/pkg/log" "github.com/openshift/odo/pkg/machineoutput" "github.com/openshift/odo/pkg/odo/genericclioptions" + "github.com/openshift/odo/pkg/odo/util/experimental" + "github.com/openshift/odo/pkg/util" "github.com/spf13/cobra" k8sgenclioptions "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/kubernetes/pkg/kubectl/util/templates" @@ -15,10 +15,13 @@ import ( // PortForwardOptions contains all the options for running the port-forward cli command. type InfoOptions struct { - Namespace string - PortForwarder *debug.DefaultPortForwarder + componentName string + applicationName string + Namespace string + PortForwarder *debug.DefaultPortForwarder *genericclioptions.Context - contextDir string + contextDir string + DevfilePath string } var ( @@ -43,12 +46,26 @@ func NewInfoOptions() *InfoOptions { // Complete completes all the required options for port-forward cmd. func (o *InfoOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) { - o.Context = genericclioptions.NewContext(cmd) - cfg, err := config.NewLocalConfigInfo(o.contextDir) - o.LocalConfigInfo = cfg + if experimental.IsExperimentalModeEnabled() && util.CheckPathExists(o.DevfilePath) { + o.Context = genericclioptions.NewDevfileContext(cmd) + + // a small shortcut + env := o.Context.EnvSpecificInfo + + o.componentName = env.GetName() + o.Namespace = env.GetNamespace() + } else { + o.Context = genericclioptions.NewContext(cmd) + cfg := o.Context.LocalConfigInfo + o.LocalConfigInfo = cfg + + o.componentName = cfg.GetName() + o.applicationName = cfg.GetApplication() + o.Namespace = cfg.GetProject() + } // Using Discard streams because nothing important is logged - o.PortForwarder = debug.NewDefaultPortForwarder(cfg.GetName(), cfg.GetApplication(), o.Client, k8sgenclioptions.NewTestIOStreamsDiscard()) + o.PortForwarder = debug.NewDefaultPortForwarder(o.componentName, o.applicationName, o.Namespace, o.Client, o.KClient, k8sgenclioptions.NewTestIOStreamsDiscard()) return err } @@ -67,7 +84,7 @@ func (o InfoOptions) Run() error { log.Infof("Debug is running for the component on the local port : %v", debugFileInfo.Spec.LocalPort) } } else { - return fmt.Errorf("debug is not running for the component %v", o.LocalConfigInfo.GetName()) + return fmt.Errorf("debug is not running for the component %v", o.componentName) } return nil } @@ -87,6 +104,9 @@ func NewCmdInfo(name, fullName string) *cobra.Command { }, } genericclioptions.AddContextFlag(cmd, &opts.contextDir) + if experimental.IsExperimentalModeEnabled() { + cmd.Flags().StringVar(&opts.DevfilePath, "devfile", "./devfile.yaml", "Path to a devfile.yaml") + } return cmd } diff --git a/pkg/odo/cli/debug/portforward.go b/pkg/odo/cli/debug/portforward.go index e162c012f9b..e666eb8d7fe 100644 --- a/pkg/odo/cli/debug/portforward.go +++ b/pkg/odo/cli/debug/portforward.go @@ -6,6 +6,7 @@ import ( "github.com/openshift/odo/pkg/debug" "github.com/openshift/odo/pkg/log" "github.com/openshift/odo/pkg/odo/genericclioptions" + "github.com/openshift/odo/pkg/odo/util/experimental" "github.com/openshift/odo/pkg/util" "net" "os" @@ -21,7 +22,10 @@ import ( // PortForwardOptions contains all the options for running the port-forward cli command. type PortForwardOptions struct { - Namespace string + componentName string + applicationName string + Namespace string + // PortPair is the combination of local and remote port in the format "local:remote" PortPair string @@ -34,6 +38,9 @@ type PortForwardOptions struct { // ReadChannel is used to receive status of port forwarding ( ready or not ready ) ReadyChannel chan struct{} *genericclioptions.Context + DevfilePath string + + isExperimental bool } var ( @@ -64,12 +71,32 @@ func NewPortForwardOptions() *PortForwardOptions { // Complete completes all the required options for port-forward cmd. func (o *PortForwardOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) { - // this populates the LocalConfigInfo - o.Context = genericclioptions.NewContext(cmd) + var remotePort int + + o.isExperimental = experimental.IsExperimentalModeEnabled() - // a small shortcut - cfg := o.Context.LocalConfigInfo - remotePort := cfg.GetDebugPort() + if o.isExperimental && util.CheckPathExists(o.DevfilePath) { + o.Context = genericclioptions.NewDevfileContext(cmd) + + // a small shortcut + env := o.Context.EnvSpecificInfo + remotePort = env.GetDebugPort() + + o.componentName = env.GetName() + o.Namespace = env.GetNamespace() + + } else { + // this populates the LocalConfigInfo + o.Context = genericclioptions.NewContext(cmd) + + // a small shortcut + cfg := o.Context.LocalConfigInfo + remotePort = cfg.GetDebugPort() + + o.componentName = cfg.GetName() + o.applicationName = cfg.GetApplication() + o.Namespace = cfg.GetProject() + } // try to listen on the given local port and check if the port is free or not addressLook := "localhost:" + strconv.Itoa(o.localPort) @@ -97,7 +124,7 @@ func (o *PortForwardOptions) Complete(name string, cmd *cobra.Command, args []st o.PortPair = fmt.Sprintf("%d:%d", o.localPort, remotePort) // Using Discard streams because nothing important is logged - o.PortForwarder = debug.NewDefaultPortForwarder(cfg.GetName(), cfg.GetApplication(), o.Client, k8sgenclioptions.NewTestIOStreamsDiscard()) + o.PortForwarder = debug.NewDefaultPortForwarder(o.componentName, o.applicationName, o.Namespace, o.Client, o.KClient, k8sgenclioptions.NewTestIOStreamsDiscard()) o.StopChannel = make(chan struct{}, 1) o.ReadyChannel = make(chan struct{}) @@ -123,7 +150,7 @@ func (o PortForwardOptions) Run() error { syscall.SIGTERM, syscall.SIGQUIT) defer signal.Stop(signals) - defer os.RemoveAll(debug.GetDebugInfoFilePath(o.Client, o.LocalConfigInfo.GetName(), o.LocalConfigInfo.GetApplication())) + defer os.RemoveAll(debug.GetDebugInfoFilePath(o.componentName, o.applicationName, o.Namespace)) go func() { <-signals @@ -137,7 +164,7 @@ func (o PortForwardOptions) Run() error { return err } - return o.PortForwarder.ForwardPorts(o.PortPair, o.StopChannel, o.ReadyChannel) + return o.PortForwarder.ForwardPorts(o.PortPair, o.StopChannel, o.ReadyChannel, o.isExperimental) } // NewCmdPortForward implements the port-forward odo command @@ -154,6 +181,9 @@ func NewCmdPortForward(name, fullName string) *cobra.Command { }, } genericclioptions.AddContextFlag(cmd, &opts.contextDir) + if experimental.IsExperimentalModeEnabled() { + cmd.Flags().StringVar(&opts.DevfilePath, "devfile", "./devfile.yaml", "Path to a devfile.yaml") + } cmd.Flags().IntVarP(&opts.localPort, "local-port", "l", config.DefaultDebugPort, "Set the local port") return cmd diff --git a/pkg/testingutil/devfile.go b/pkg/testingutil/devfile.go index 303d8b4c3a3..7ddd611cbe6 100644 --- a/pkg/testingutil/devfile.go +++ b/pkg/testingutil/devfile.go @@ -94,7 +94,7 @@ func (d TestDevfileData) GetProjects() []versionsCommon.DevfileProject { // GetCommands is a mock function to get the commands from a devfile func (d TestDevfileData) GetCommands() []versionsCommon.DevfileCommand { - commandName := [...]string{"devinit", "devbuild", "devrun", "customcommand"} + commandName := [...]string{"devinit", "devbuild", "devrun", "devdebug", "customcommand", "customdebugcommand"} commands := []versionsCommon.DevfileCommand{ { @@ -105,6 +105,14 @@ func (d TestDevfileData) GetCommands() []versionsCommon.DevfileCommand { Name: commandName[3], Actions: d.CommandActions, }, + { + Name: commandName[4], + Actions: d.CommandActions, + }, + { + Name: commandName[5], + Actions: d.CommandActions, + }, } if !d.MissingInitCommand { commands = append(commands, versionsCommon.DevfileCommand{ diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-debugrun.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-debugrun.yaml new file mode 100644 index 00000000000..2116fbe0607 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-debugrun.yaml @@ -0,0 +1,40 @@ +apiVersion: 1.0.0 +metadata: + name: test-devfile +projects: + - + name: nodejs-web-app + source: + type: git + location: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - type: dockerimage + image: quay.io/eclipse/che-nodejs10-ubi:nightly + endpoints: + - name: "3000/tcp" + port: 3000 + alias: runtime + env: + - name: FOO + value: "bar" + memoryLimit: 1024Mi + mountSources: true +commands: + - name: devbuild + actions: + - type: exec + component: runtime + command: "npm install" + workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + - name: devrun + actions: + - type: exec + component: runtime + command: "nodemon app.js" + workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + - name: devdebug + actions: + - type: exec + component: runtime + command: "nodemon --inspect=${DEBUG_PORT}" + workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/ diff --git a/tests/integration/debug/cmd_debug_test.go b/tests/integration/debug/cmd_debug_test.go index 515ba65ed7e..a59202aebaf 100644 --- a/tests/integration/debug/cmd_debug_test.go +++ b/tests/integration/debug/cmd_debug_test.go @@ -2,6 +2,7 @@ package debug import ( "github.com/openshift/odo/pkg/config" + "github.com/openshift/odo/pkg/envinfo" "github.com/openshift/odo/pkg/testingutil" "github.com/openshift/odo/tests/helper" "os" @@ -21,6 +22,9 @@ var _ = Describe("odo debug command serial tests", func() { var project string var context string + var namespace, componentName, projectDirPath string + var projectDir = "/projectDir" + // Setup up state for each test spec // create new project (not set as active) and new context directory for each test spec // This is before every spec (It) @@ -30,6 +34,12 @@ var _ = Describe("odo debug command serial tests", func() { context = helper.CreateNewContext() project = helper.CreateRandProject() os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml")) + componentName = helper.RandString(6) + + namespace = helper.CreateRandProject() + context = helper.CreateNewContext() + projectDirPath = context + projectDir + }) // Clean up after the test @@ -66,7 +76,63 @@ var _ = Describe("odo debug command serial tests", func() { } freePort := "" - helper.WaitForCmdOut("odo", []string{"debug", "info", "--context", context}, 1, true, func(output string) bool { + helper.WaitForCmdOut("odo", []string{"debug", "info", "--context", context}, 1, false, func(output string) bool { + if strings.Contains(output, "Debug is running") { + splits := strings.SplitN(output, ":", 2) + Expect(len(splits)).To(Equal(2)) + freePort = strings.TrimSpace(splits[1]) + _, err := strconv.Atoi(freePort) + Expect(err).NotTo(HaveOccurred()) + return true + } + return false + }) + + // 400 response expected because the endpoint expects a websocket request and we are doing a HTTP GET + // We are just using this to validate if nodejs agent is listening on the other side + helper.HttpWaitForWithStatus("http://localhost:"+freePort, "WebSockets request was expected", 12, 5, 400) + stopChannel <- true + if listenerStarted == true { + stopListenerChan <- true + } else { + close(stopListenerChan) + } + }) + + It("should auto-select a local debug port when the given local port is occupied for a devfile component", func() { + // Devfile push requires experimental mode to be set + helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true", "-f") + + helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, componentName) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) + helper.CmdShouldPass("odo", "push", "--devfile", "devfile-with-debugrun.yaml", "--debug") + + stopChannel := make(chan bool) + go func() { + helper.CmdShouldRunAndTerminate(60*time.Second, stopChannel, "odo", "debug", "port-forward") + }() + + stopListenerChan := make(chan bool) + startListenerChan := make(chan bool) + listenerStarted := false + go func() { + defer GinkgoRecover() + err := testingutil.FakePortListener(startListenerChan, stopListenerChan, envinfo.DefaultDebugPort) + if err != nil { + close(startListenerChan) + Expect(err).Should(BeNil()) + } + }() + // wait for the test server to start listening + if <-startListenerChan { + listenerStarted = true + } + + freePort := "" + helper.WaitForCmdOut("odo", []string{"debug", "info"}, 1, false, func(output string) bool { if strings.Contains(output, "Debug is running") { splits := strings.SplitN(output, ":", 2) Expect(len(splits)).To(Equal(2)) diff --git a/tests/integration/devfile/cmd_devfile_debug_test.go b/tests/integration/devfile/cmd_devfile_debug_test.go new file mode 100644 index 00000000000..1ef5b5e2fe4 --- /dev/null +++ b/tests/integration/devfile/cmd_devfile_debug_test.go @@ -0,0 +1,171 @@ +package devfile + +import ( + "github.com/openshift/odo/pkg/util" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/openshift/odo/tests/helper" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("odo devfile debug command tests", func() { + var namespace, context, componentName, currentWorkingDirectory, projectDirPath string + var projectDir = "/projectDir" + + // This is run after every Spec (It) + var _ = BeforeEach(func() { + SetDefaultEventuallyTimeout(10 * time.Minute) + SetDefaultConsistentlyDuration(30 * time.Second) + namespace = helper.CreateRandProject() + context = helper.CreateNewContext() + currentWorkingDirectory = helper.Getwd() + projectDirPath = context + projectDir + componentName = helper.RandString(6) + + helper.Chdir(context) + + os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml")) + + // Devfile push requires experimental mode to be set + helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") + }) + + // Clean up after the test + // This is run after every Spec (It) + var _ = AfterEach(func() { + helper.DeleteProject(namespace) + helper.Chdir(currentWorkingDirectory) + helper.DeleteDir(context) + os.Unsetenv("GLOBALODOCONFIG") + }) + + Context("odo debug on a nodejs:latest component", func() { + It("check that machine output debug information works", func() { + helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, componentName) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) + helper.CmdShouldPass("odo", "push", "--devfile", "devfile-with-debugrun.yaml", "--debug") + + httpPort, err := util.HttpGetFreePort() + Expect(err).NotTo(HaveOccurred()) + freePort := strconv.Itoa(httpPort) + + stopChannel := make(chan bool) + go func() { + helper.CmdShouldRunAndTerminate(60*time.Second, stopChannel, "odo", "debug", "port-forward", "--local-port", freePort) + }() + + // Make sure that the debug information output, outputs correctly. + // We do *not* check the json output since the debugProcessID will be different each time. + helper.WaitForCmdOut("odo", []string{"debug", "info", "-o", "json"}, 1, false, func(output string) bool { + if strings.Contains(output, `"kind": "OdoDebugInfo"`) && + strings.Contains(output, `"localPort": `+freePort) { + return true + } + return false + }) + + stopChannel <- true + }) + + It("should expect a ws connection when tried to connect on default debug port locally", func() { + helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, componentName) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) + helper.CmdShouldPass("odo", "push", "--devfile", "devfile-with-debugrun.yaml") + helper.CmdShouldPass("odo", "push", "--devfile", "devfile-with-debugrun.yaml", "--debug") + + stopChannel := make(chan bool) + go func() { + helper.CmdShouldRunAndTerminate(60*time.Second, stopChannel, "odo", "debug", "port-forward") + }() + + // 400 response expected because the endpoint expects a websocket request and we are doing a HTTP GET + // We are just using this to validate if nodejs agent is listening on the other side + helper.HttpWaitForWithStatus("http://localhost:5858", "WebSockets request was expected", 12, 5, 400) + stopChannel <- true + }) + + }) + + Context("odo debug info should work on a odo component", func() { + It("should start a debug session and run debug info on a running debug session", func() { + helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "nodejs", "nodejs-cmp-"+namespace, "--project", namespace) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) + helper.CmdShouldPass("odo", "push", "--devfile", "devfile-with-debugrun.yaml", "--debug") + + httpPort, err := util.HttpGetFreePort() + Expect(err).NotTo(HaveOccurred()) + freePort := strconv.Itoa(httpPort) + + stopChannel := make(chan bool) + go func() { + helper.CmdShouldRunAndTerminate(60*time.Second, stopChannel, "odo", "debug", "port-forward", "--local-port", freePort) + }() + + // 400 response expected because the endpoint expects a websocket request and we are doing a HTTP GET + // We are just using this to validate if nodejs agent is listening on the other side + helper.HttpWaitForWithStatus("http://localhost:"+freePort, "WebSockets request was expected", 12, 5, 400) + runningString := helper.CmdShouldPass("odo", "debug", "info") + Expect(runningString).To(ContainSubstring(freePort)) + Expect(helper.ListFilesInDir(os.TempDir())).To(ContainElement(namespace + "-nodejs-cmp-" + namespace + "-odo-debug.json")) + stopChannel <- true + }) + + It("should start a debug session and run debug info on a closed debug session", func() { + helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, componentName) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) + helper.CmdShouldPass("odo", "push", "--devfile", "devfile-with-debugrun.yaml", "--debug") + + httpPort, err := util.HttpGetFreePort() + Expect(err).NotTo(HaveOccurred()) + freePort := strconv.Itoa(httpPort) + + stopChannel := make(chan bool) + go func() { + helper.CmdShouldRunAndTerminate(60*time.Second, stopChannel, "odo", "debug", "port-forward", "--local-port", freePort) + }() + + // 400 response expected because the endpoint expects a websocket request and we are doing a HTTP GET + // We are just using this to validate if nodejs agent is listening on the other side + helper.HttpWaitForWithStatus("http://localhost:"+freePort, "WebSockets request was expected", 12, 5, 400) + runningString := helper.CmdShouldPass("odo", "debug", "info") + Expect(runningString).To(ContainSubstring(freePort)) + stopChannel <- true + failString := helper.CmdShouldFail("odo", "debug", "info") + Expect(failString).To(ContainSubstring("not running")) + + // according to https://golang.org/pkg/os/#Signal On Windows, sending os.Interrupt to a process with os.Process.Signal is not implemented + // discussion on the go repo https://github.com/golang/go/issues/6720 + // session.Interrupt() will not work as it internally uses syscall.SIGINT + // thus debug port-forward won't stop running + // the solution is to use syscall.SIGKILL for windows but this will kill the process immediately + // and the cleaning and closing tasks for debug port-forward won't run and the debug info file won't be cleared + // thus we skip this last check + // CTRL_C_EVENTS from the terminal works fine https://github.com/golang/go/issues/6720#issuecomment-66087737 + // here's a hack to generate the event https://golang.org/cl/29290044 + // but the solution is unacceptable https://github.com/golang/go/issues/6720#issuecomment-66087749 + if runtime.GOOS != "windows" { + Expect(helper.ListFilesInDir(os.TempDir())).To(Not(ContainElement(namespace + "-app" + "-nodejs-cmp-" + namespace + "-odo-debug.json"))) + } + + }) + }) +})