From a2bae966b17fd3b2c20d6916bfd8d0db22b0a491 Mon Sep 17 00:00:00 2001 From: mik-dass Date: Mon, 20 Jan 2020 15:01:11 +0530 Subject: [PATCH] Adds the debug info command --- pkg/debug/info.go | 124 ++++++++++++++++++++++++++++ pkg/debug/info_test.go | 117 ++++++++++++++++++++++++++ pkg/odo/cli/debug/debug.go | 2 + pkg/odo/cli/debug/info.go | 85 +++++++++++++++++++ pkg/odo/cli/debug/portforward.go | 18 +++- tests/helper/helper_run.go | 22 +++++ tests/integration/cmd_debug_test.go | 43 ++++++++++ 7 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 pkg/debug/info.go create mode 100644 pkg/debug/info_test.go create mode 100644 pkg/odo/cli/debug/info.go diff --git a/pkg/debug/info.go b/pkg/debug/info.go new file mode 100644 index 00000000000..87cc7c7d4b4 --- /dev/null +++ b/pkg/debug/info.go @@ -0,0 +1,124 @@ +package debug + +import ( + "encoding/json" + "errors" + "github.com/golang/glog" + "github.com/openshift/odo/pkg/occlient" + "github.com/openshift/odo/pkg/testingutil/filesystem" + "io/ioutil" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +type OdoDebugFile struct { + metav1.TypeMeta + DebugProcessId int + ProjectName string + AppName string + ComponentName string + RemotePort int + LocalPort int +} + +// GetDebugInfoFilePath gets the file path of the debug info file +func GetDebugInfoFilePath(client *occlient.Client, componentName, appName string) string { + tempDir := os.TempDir() + debugFilePrefix := "odo-debug.json" + s := []string{client.Namespace, appName, componentName, debugFilePrefix} + debugFileName := strings.Join(s, "-") + return filepath.Join(tempDir, debugFileName) +} + +func CreateDebugInfoFile(f *DefaultPortForwarder, portPair string) error { + return createDebugInfoFile(f, portPair, filesystem.DefaultFs{}) +} + +// createDebugInfoFile creates a file in the temp directory with information regarding the debugging session of a component +func createDebugInfoFile(f *DefaultPortForwarder, portPair string, fs filesystem.Filesystem) error { + portPairs := strings.Split(portPair, ":") + if len(portPairs) > 2 || len(portPairs) < 2 { + return errors.New("port pair should be of the format localPort:RemotePair") + } + + localPort, err := strconv.Atoi(portPairs[0]) + if err != nil { + return errors.New("local port should be a int") + } + remotePort, err := strconv.Atoi(portPairs[1]) + if err != nil { + return errors.New("remote port should be a int") + } + + odoDebugFile := OdoDebugFile{ + TypeMeta: metav1.TypeMeta{}, + DebugProcessId: os.Getpid(), + ProjectName: f.client.Namespace, + AppName: f.appName, + ComponentName: f.componentName, + RemotePort: remotePort, + LocalPort: localPort, + } + odoDebugPathData, err := json.Marshal(odoDebugFile) + if err != nil { + return errors.New("error marshalling json data") + } + + // 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) + if err != nil { + return err + } + _, err = file.Write(odoDebugPathData) + if err != nil { + return err + } + return nil +} + +// GetDebugInfo gets information regarding the debugging session of the component +func GetDebugInfo(f *DefaultPortForwarder) (int, bool) { + // gets the debug info file path and reads/unmarshals it + debugInfoFilePath := GetDebugInfoFilePath(f.client, f.componentName, f.appName) + odoDebugFileRead, err := ioutil.ReadFile(debugInfoFilePath) + if err != nil { + glog.V(4).Infof("the debug %v is not present", debugInfoFilePath) + return -1, false + } + + var odoDebugFileData OdoDebugFile + err = json.Unmarshal(odoDebugFileRead, &odoDebugFileData) + if err != nil { + glog.V(4).Infof("couldn't unmarshal the debug file %v", debugInfoFilePath) + return -1, false + } + + // get the debug process id and send a signal 0 to check if it's alive or not + processInfo, err := os.FindProcess(odoDebugFileData.DebugProcessId) + if err != nil { + glog.V(4).Infof("error getting the process info for pid %v", odoDebugFileData.DebugProcessId) + return -1, false + } + + err = processInfo.Signal(syscall.Signal(0)) + if err != nil { + glog.V(4).Infof("error sending signal 0 to pid %v, cause: %v", odoDebugFileData.DebugProcessId, err) + return -1, false + } + + // gets the debug local port and dials it to check if the port is listening or not + addressLook := "localhost:" + strconv.Itoa(odoDebugFileData.LocalPort) + _, err = net.Dial("tcp", addressLook) + if err != nil { + glog.V(4).Infof("error dialing address %v, cause: %v", odoDebugFileData.DebugProcessId, err) + return -1, false + } + + // returns the local port for further processing + return odoDebugFileData.LocalPort, true +} diff --git a/pkg/debug/info_test.go b/pkg/debug/info_test.go new file mode 100644 index 00000000000..c0ad5291350 --- /dev/null +++ b/pkg/debug/info_test.go @@ -0,0 +1,117 @@ +package debug + +import ( + "encoding/json" + "github.com/openshift/odo/pkg/occlient" + "github.com/openshift/odo/pkg/testingutil/filesystem" + "os" + "testing" +) + +func Test_createDebugInfoFile(t *testing.T) { + + // create a fake fs in memory + fs := filesystem.NewFakeFs() + + type args struct { + defaultPortForwarder *DefaultPortForwarder + portPair string + fs filesystem.Filesystem + } + tests := []struct { + name string + args args + wantLocalPort int + wantRemotePort int + alreadyExistFile bool + wantErr bool + }{ + { + name: "case 1: normal json write to the debug file", + args: args{ + defaultPortForwarder: &DefaultPortForwarder{ + componentName: "nodejs-ex", + appName: "app", + }, + portPair: "5858:9001", + fs: fs, + }, + wantLocalPort: 5858, + wantRemotePort: 9001, + alreadyExistFile: false, + wantErr: false, + }, + { + name: "case 2: overwrite the debug file", + args: args{ + defaultPortForwarder: &DefaultPortForwarder{ + componentName: "nodejs-ex", + appName: "app", + }, + portPair: "5758:9004", + fs: fs, + }, + wantLocalPort: 5758, + wantRemotePort: 9004, + alreadyExistFile: true, + wantErr: false, + }, + } + 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) + if tt.alreadyExistFile { + file, err := fs.Create(debugFilePath) + if err != nil { + t.Errorf("error happened while writing, cause: %v", err) + } + _, err = file.WriteString("blah") + if err != nil { + t.Errorf("error happened while writing, cause: %v", err) + } + } + + if err := createDebugInfoFile(tt.args.defaultPortForwarder, tt.args.portPair, tt.args.fs); (err != nil) != tt.wantErr { + t.Errorf("createDebugInfoFile() error = %v, wantErr %v", err, tt.wantErr) + } + + readBytes, err := fs.ReadFile(debugFilePath) + if err != nil { + t.Errorf("error while reading file, cause: %v", err) + } + var odoDebugFileData OdoDebugFile + err = json.Unmarshal(readBytes, &odoDebugFileData) + if err != nil { + t.Errorf("error occured while unmarshalling json, cause: %v", err) + } + + if odoDebugFileData.LocalPort != tt.wantLocalPort { + t.Errorf("the local port on the file doesn't match, got %v, want %v", odoDebugFileData.LocalPort, tt.wantLocalPort) + } + if odoDebugFileData.RemotePort != tt.wantRemotePort { + t.Errorf("the remote port on the file doesn't match, got %v, want %v", odoDebugFileData.RemotePort, tt.wantRemotePort) + } + if odoDebugFileData.DebugProcessId != os.Getpid() { + t.Errorf("the debug process id on the file doesn't match, got %v, want %v", odoDebugFileData.DebugProcessId, os.Getpid()) + } + if odoDebugFileData.ComponentName != tt.args.defaultPortForwarder.componentName { + t.Errorf("the component name on the file doesn't match, got %v, want %v", odoDebugFileData.ComponentName, tt.args.defaultPortForwarder.componentName) + } + if odoDebugFileData.AppName != tt.args.defaultPortForwarder.appName { + t.Errorf("the app name on the file doesn't match, got %v, want %v", odoDebugFileData.AppName, tt.args.defaultPortForwarder.appName) + } + if odoDebugFileData.AppName != tt.args.defaultPortForwarder.appName { + t.Errorf("the app name on the file doesn't match, got %v, want %v", odoDebugFileData.AppName, tt.args.defaultPortForwarder.appName) + } + if odoDebugFileData.ProjectName != tt.args.defaultPortForwarder.client.Namespace { + t.Errorf("the app name on the file doesn't match, got %v, want %v", odoDebugFileData.AppName, tt.args.defaultPortForwarder.client.Namespace) + } + }) + } +} diff --git a/pkg/odo/cli/debug/debug.go b/pkg/odo/cli/debug/debug.go index a9514df4fb4..283fbc38816 100644 --- a/pkg/odo/cli/debug/debug.go +++ b/pkg/odo/cli/debug/debug.go @@ -15,6 +15,7 @@ Debug allows you to remotely debug your application` func NewCmdDebug(name, fullName string) *cobra.Command { portforwardCmd := NewCmdPortForward(portforwardCommandName, util.GetFullName(fullName, portforwardCommandName)) + infoCmd := NewCmdInfo(infoCommandName, util.GetFullName(fullName, infoCommandName)) debugCmd := &cobra.Command{ Use: name, @@ -25,6 +26,7 @@ func NewCmdDebug(name, fullName string) *cobra.Command { debugCmd.SetUsageTemplate(util.CmdUsageTemplate) debugCmd.AddCommand(portforwardCmd) + debugCmd.AddCommand(infoCmd) debugCmd.Annotations = map[string]string{"command": "main"} return debugCmd diff --git a/pkg/odo/cli/debug/info.go b/pkg/odo/cli/debug/info.go new file mode 100644 index 00000000000..74226bd64f4 --- /dev/null +++ b/pkg/odo/cli/debug/info.go @@ -0,0 +1,85 @@ +package debug + +import ( + "github.com/openshift/odo/pkg/config" + "github.com/openshift/odo/pkg/debug" + "github.com/openshift/odo/pkg/log" + "github.com/openshift/odo/pkg/odo/genericclioptions" + "github.com/spf13/cobra" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + k8sgenclioptions "k8s.io/kubernetes/pkg/kubectl/genericclioptions" +) + +// PortForwardOptions contains all the options for running the port-forward cli command. +type InfoOptions struct { + Namespace string + PortForwarder *debug.DefaultPortForwarder + *genericclioptions.Context + localConfigInfo *config.LocalConfigInfo + contextDir string +} + +var ( + infoLong = templates.LongDesc(` + Gets information regarding any debug session of the component. + `) + + infoExample = templates.Examples(` + # Get information regarding any debug session of the component + odo debug info + + `) +) + +const ( + infoCommandName = "info" +) + +func NewInfoOptions() *InfoOptions { + return &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 + + // Using Discard streams because nothing important is logged + o.PortForwarder = debug.NewDefaultPortForwarder(cfg.GetName(), cfg.GetApplication(), o.Client, k8sgenclioptions.NewTestIOStreamsDiscard()) + + return err +} + +// Validate validates all the required options for port-forward cmd. +func (o InfoOptions) Validate() error { + return nil +} + +// Run implements all the necessary functionality for port-forward cmd. +func (o InfoOptions) Run() error { + if localPort, debugging := debug.GetDebugInfo(o.PortForwarder); debugging { + log.Infof("Debug is running for the component on the local port : %v\n", localPort) + } else { + log.Infof("Debug is not running for the component %v\n", o.localConfigInfo.GetName()) + } + return nil +} + +// NewCmdInfo implements the debug info odo command +func NewCmdInfo(name, fullName string) *cobra.Command { + + opts := NewInfoOptions() + cmd := &cobra.Command{ + Use: name, + Short: "Displays debug info of a component", + Long: infoLong, + Example: infoExample, + Run: func(cmd *cobra.Command, args []string) { + genericclioptions.GenericRun(opts, cmd, args) + }, + } + genericclioptions.AddContextFlag(cmd, &opts.contextDir) + + return cmd +} diff --git a/pkg/odo/cli/debug/portforward.go b/pkg/odo/cli/debug/portforward.go index c3d406cb87f..743a22be913 100644 --- a/pkg/odo/cli/debug/portforward.go +++ b/pkg/odo/cli/debug/portforward.go @@ -2,12 +2,12 @@ package debug import ( "fmt" - "os" - "os/signal" - "github.com/openshift/odo/pkg/config" "github.com/openshift/odo/pkg/debug" "github.com/openshift/odo/pkg/odo/genericclioptions" + "os" + "os/signal" + "syscall" "github.com/spf13/cobra" @@ -89,8 +89,13 @@ func (o PortForwardOptions) Validate() error { func (o PortForwardOptions) Run() error { signals := make(chan os.Signal, 1) - signal.Notify(signals, os.Interrupt) + signal.Notify(signals, os.Interrupt, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT) defer signal.Stop(signals) + defer os.RemoveAll(debug.GetDebugInfoFilePath(o.Client, o.localConfigInfo.GetName(), o.localConfigInfo.GetApplication())) go func() { <-signals @@ -99,6 +104,11 @@ func (o PortForwardOptions) Run() error { } }() + err := debug.CreateDebugInfoFile(o.PortForwarder, o.PortPair) + if err != nil { + return err + } + return o.PortForwarder.ForwardPorts(o.PortPair, o.StopChannel, o.ReadyChannel) } diff --git a/tests/helper/helper_run.go b/tests/helper/helper_run.go index 4f2a8ac5028..e1aae89f76e 100644 --- a/tests/helper/helper_run.go +++ b/tests/helper/helper_run.go @@ -42,6 +42,28 @@ func CmdShouldRunWithTimeout(timeout time.Duration, program string, args ...stri return string(session.Out.Contents()) } +// CmdShouldRunAndTerminate waits and returns stdout after a closed signal is passed on the closed channel +func CmdShouldRunAndTerminate(timeoutAfter time.Duration, stopChan chan int, program string, args ...string) string { + session := CmdRunner(program, args...) + timeout := time.After(timeoutAfter) + select { + case <-stopChan: + if session != nil { + session.Terminate() + } + case <-timeout: + if session != nil { + session.Terminate() + } + } + + if session == nil { + return "" + } + + return string(session.Out.Contents()) +} + // CmdShouldFail returns stderr if command fails func CmdShouldFail(program string, args ...string) string { session := CmdRunner(program, args...) diff --git a/tests/integration/cmd_debug_test.go b/tests/integration/cmd_debug_test.go index 16e6dcef703..d8ae00bb719 100644 --- a/tests/integration/cmd_debug_test.go +++ b/tests/integration/cmd_debug_test.go @@ -65,4 +65,47 @@ var _ = Describe("odo debug command tests", func() { }) }) + + 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.CopyExample(filepath.Join("source", "nodejs"), context) + helper.CmdShouldPass("odo", "component", "create", "nodejs:8", "nodejs-cmp-"+project, "--project", project, "--context", context) + helper.CmdShouldPass("odo", "push", "--context", context) + + stopChannel := make(chan int) + go func() { + helper.CmdShouldRunAndTerminate(60*time.Second, stopChannel, "odo", "debug", "port-forward", "--local-port", "9000", "--context", context) + }() + + // 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:9000", "WebSockets request was expected", 12, 5, 400) + runningString := helper.CmdShouldPass("odo", "debug", "info", "--context", context) + Expect(runningString).To(ContainSubstring("9000")) + Expect(helper.ListFilesInDir(os.TempDir())).To(ContainElement(project + "-app" + "-nodejs-cmp-" + project + "-odo-debug.json")) + stopChannel <- 0 + }) + + It("should start a debug session and run debug info on a closed debug session", func() { + helper.CopyExample(filepath.Join("source", "nodejs"), context) + helper.CmdShouldPass("odo", "component", "create", "nodejs:8", "nodejs-cmp-"+project, "--project", project, "--context", context) + helper.CmdShouldPass("odo", "push", "--context", context) + + stopChannel := make(chan int) + go func() { + helper.CmdShouldRunAndTerminate(60*time.Second, stopChannel, "odo", "debug", "port-forward", "--local-port", "9001", "--context", context) + }() + + // 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:9001", "WebSockets request was expected", 12, 5, 400) + runningString := helper.CmdShouldPass("odo", "debug", "info", "--context", context) + Expect(runningString).To(ContainSubstring("9001")) + stopChannel <- 0 + failString := helper.CmdShouldPass("odo", "debug", "info", "--context", context) + Expect(failString).To(ContainSubstring("not running")) + Expect(helper.ListFilesInDir(os.TempDir())).To(Not(ContainElement(project + "-app" + "-nodejs-cmp-" + project + "-odo-debug.json"))) + + }) + }) })